Azure Veneziano – 第一部分





5.00/5 (2投票s)
如何使用 Azure 创建您自己的遥测控件
这是系列的第一部分,我将把一个遥测项目作为一个经典的“物联网”展示。该项目一开始非常基础,但在后续部分中会通过添加各种有用的组件来不断完善。
核心角色是 Microsoft Azure,但其他部分将涉及多种技术。
该项目的源代码托管在 azure-veneziano GitHub 仓库中。
灵感
该项目最初是一个沙盒,用于深入研究可能适用于我们的控制系统的云技术。我想遍历真实控制系统(如果喜欢,可以称之为 SCADA)的几乎每一个角落,以了解完全集中式解决方案的优点和局限性。
顺便说一句,我也受到了我的朋友 Laurent Ellerbach 的启发,他发表了一篇非常精彩的文章,介绍如何构建自己的花园洒水系统。总的来说,我喜欢不同组件混合在一起(即互联)的方式:似乎我们正面临一个里程碑,这些技术的灵活性超出了我们的想象。
在我写这篇文章的时候,Laurent 正在将他的文章从法语翻译成英语,所以我还在等待新的链接。同时,这里是最近在乌克兰基辅举行的一次类似的演讲。
更新:Laurent 的文章现在可以在这里找到。
为什么命名为“Azure Veneziano”?
如果你们中有谁有机会参观过我的城市,可能也亲眼见过一些著名的穆拉诺岛(Murano)玻璃制造师的作品,而“Blu Veneziano”(威尼斯蓝)是一种特殊的蓝色调,常用于玻璃制品。
我只是想向威尼斯致敬,同时也提及所用框架的“颜色”,因此得名!
系统结构
系统结构为生产者-消费者模式,其中:
- 数据生产者是一个(或多个)“移动设备”,它们采集并有时收集传感器数据
- 数据代理、存储和业务层部署在 Azure 上,主要逻辑在此运行
- 数据消费者既包括逻辑,也包括最终用户(在本例中是我自己),他们监控系统
在这篇入门文章中,我将重点介绍第一部分,使用一块 Netduino Plus 2 板作为数据生产者。
Netduino 作为数据生产者
在物联网视角下,Netduino扮演“移动设备”的角色。基本上,它充当了一个硬件-软件的薄接口,以便将转换后的数据发送到服务器(在本例中是 Azure)。可以想象一个温度传感器,连接到 ADC,逻辑读取数值并发送到 Azure。但是,我这里不会详细介绍一个“真实传感器”系统,而是做一个简单的模拟,任何人都可以轻松完成。
此外,由于我将项目定义为“遥测”,数据流仅从 Netduino“传出”。这意味着(目前)不支持向板发送“命令”。让我们坚持使用最简单的实现方式。
硬件
电路非常简单。
两个微调电位器:每个微调电位器向 respective 模拟输入提供 0.0 到 3.3 V 的电压。也就是说,Netduino 的内部 ADC 会将电压转换为浮点(Double
)值,范围从 0.0
到 100.0
(为了方便阅读,意味着它相当于百分比)。
还有两个拨动开关。每个开关连接到一个离散(布尔)输入,应配置为内部上拉。当开关打开时,上拉电阻将输入值拉高(true
)。当开关接地时,由于其电阻低于上拉电阻,它将值拉低。以及两个开关。
如果注意到,每个开关串联了一个低值电阻:我使用的是 270 欧姆的电阻,但这并不重要。其目的只是为了保护 Netduino 输入免受错误影响。想象一下,一个引脚被错误地配置为输出:如果开关接地时输出设置为高电平怎么办?可能不会烧毁,但该端口承受的压力不是好事。
从程序员的角度来看,所有这些“虚拟”传感器都可以看作是两个 Double
值和两个布尔值。有趣的是,我可以用手指修改它们的值!
同样,这里的真实传感器是什么并不重要。我想为那些不太喜欢/理解电子学的人们改造硬件部分。有许多即用型模块/扩展板可以连接,可以避免(或最小化)处理硬件的机会。
一些虚拟端口和我的……懒惰。
相信我,我很懒。
尽管我非常享受玩弄这些软硬件的乐趣,但我真的不喜欢一直转动微调电位器或滑动开关,但我需要一些随时间变化的数据。所以,我创建了一种(软件)虚拟端口。
这个端口将在下面详细介绍,它的任务是模拟一个“真实”的硬件端口。从数据生产的角度来看,它与真实端口没有区别,但管理起来要容易得多,尤其是在测试/演示会话中。
这种“虚拟端口”的概念在高阶系统中也非常普遍。只需想想设备的诊断部分,它从非物理源(例如内存使用量、CPU 使用量等)收集数据。
软件
由于目标是将 Netduino 读取的数据发布到服务器,因此我们应该仔细选择最佳方法。
将 Netduino Plus 2 连接到外界的最简单方法是使用以太网线。至少对于原型来说,这是可以的,因为目标是连接到互联网。
关于协议,在与 Azure 交换数据的多种协议中,我认为最简单但又广为人知的方法是使用 HTTP。还要记住,当前的 Netduino/.NET Micro Framework 实现中没有任何“特殊”协议。
板上运行的软件非常简单。其结构如下:
- 主应用程序,作为设备的主要逻辑
- 一些硬件端口包装器作为数据捕获助手
- 为 Azure 移动数据交换优化的 HTTP 客户端
- 具有序列化/反序列化功能的 JSON DOM
数据传输是正常的 HTTP。在撰写本文时,.NET Micro-Framework 仍然不提供任何 HTTPS 支持,因此数据是未加密传输的。
主应用程序的第一部分是关于端口定义。它与经典的声明没有特别不同,但端口被“包装”在自定义代码中。
/**
* Hardware input ports definition
**/
private static InputPortWrapper _switch0 = new InputPortWrapper(
"Switch0",
Pins.GPIO_PIN_D0
);
private static InputPortWrapper _switch1 = new InputPortWrapper(
"Switch1",
Pins.GPIO_PIN_D1
);
private static AnalogInputWrapper _analog0 = new AnalogInputWrapper(
"Analog0",
AnalogChannels.ANALOG_PIN_A0,
100.0,
0.0
);
private static AnalogInputWrapper _analog1 = new AnalogInputWrapper(
"Analog1",
AnalogChannels.ANALOG_PIN_A1,
100.0,
0.0
);
端口包装器
端口包装器的目标是双重的:
- 为通用输入端口提供更好的抽象
- 管理“
已更改
”标志,特别是对于非离散值(如模拟值)
让我们以 AnalogInputWrapper
类为例
/// <summary>
/// Wrapper around the standard <see cref="Microsoft.SPOT.Hardware.AnalogInput"/>
/// </summary>
public class AnalogInputWrapper
: AnalogInput, IInputDouble
{
public AnalogInputWrapper(
string name,
Cpu.AnalogChannel channel,
double scale,
double offset,
double normalizedTolerance = 0.05
)
: base(channel, scale, offset, 12)
{
this.Name = name;
//precalculate the absolute variation window
//around the reference (old) sampled value
this._absoluteToleranceDelta = scale * normalizedTolerance;
}
private double _oldValue = double.NegativeInfinity; //undefined
private double _absoluteToleranceDelta;
public string Name { get; private set; }
public double Value { get; private set; }
public bool HasChanged { get; private set; }
public bool Sample()
{
this.Value = this.Read();
//detect the variation
bool hasChanged =
this.Value < (this._oldValue - this._absoluteToleranceDelta) ||
this.Value > (this._oldValue + this._absoluteToleranceDelta);
if (hasChanged)
{
//update the reference (old) value
this._oldValue = this.Value;
}
return (this.HasChanged = hasChanged);
}
// ...
}
该类派生自原始的 AnalogInput
端口,但公开“Sample
”方法来捕获 ADC 值(Read
方法)。其目的类似于经典的采样保持结构,但有一个比较算法可以检测新值的变化。
基本上,必须为端口定义一个“容差”参数(归一化)(默认为 5%)。当执行新的采样时,其值会与“旧值”进行比较,再加上旧值周围的容差窗口。当新值落在窗口之外时,官方端口的值将被标记为“已更改”,并且旧值将被新值替换。
这个技巧非常有用,因为它允许避免无谓的(和错误的)值更改。即使电源轨上的微小噪声也可能导致 ADC 标称采样值出现小的波动。但是,我们只需要“具体”的变化。
上述类也实现了 IInputDouble
接口。此接口也来自另一个更抽象
的接口。
/// <summary>
/// Double-valued input port specialization
/// </summary>
public interface IInputDouble
: IInput
{
/// <summary>
/// The sampled input port value
/// </summary>
double Value { get; }
}
/// <summary>
/// Generic input port abstraction
/// </summary>
public interface IInput
{
/// <summary>
/// Friendly name of the port
/// </summary>
string Name { get; }
/// <summary>
/// Indicate whether the port value has changed
/// </summary>
bool HasChanged { get; }
/// <summary>
/// Perform the port sampling
/// </summary>
/// <returns></returns>
bool Sample();
/// <summary>
/// Append to the container an object made up
/// with the input port status
/// </summary>
/// <param name="container"></param>
void Serialize(JArray container);
}
这些接口为不同类型的端口提供了更好的抽象:AnalogInput
、InputPort
和 RampGenerator
。
RampGenerator 作为虚拟端口
如前所述,存在一个“假包装器”,因为它不包装任何端口,但它起的作用就像一个标准端口。好处来自于接口抽象。
为了随时间“生成”数据进行演示,我想要一些自动化的但又“众所周知”的东西。我可能使用了随机数生成器,但是……如何检测随机数字流中的错误或错误序列?最好依赖一个形状完美的波形,它是周期性的,这样我就可以轻松地检查服务器上的样本顺序是否正确,以及是否有缺失/重复的数据。
作为周期信号,您可以选择任何您想要的。正弦波可能是最著名的周期波,但目标是测试系统,而不是有什么好看的东西。简单的“三角波”生成器,就是无限地线性上升然后下降的斜坡。
/// <summary>
/// Virtual input port simulating a triangle waveform
/// </summary>
public class RampGenerator
: IInputInt32
{
public RampGenerator(
string name,
int period,
int scale,
int offset
)
{
this.Name = name;
this.Period = period;
this.Scale = scale;
this.Offset = offset;
//the wave being subdivided in 40 slices
this._stepPeriod = this.Period / 40;
//vertical direction: 1=rise; -1=fall
this._rawDirection = 1;
}
// ...
public bool Sample()
{
bool hasChanged = false;
if (++this._stepTimer <= 0)
{
//very first sampling
this.Value = this.Offset;
hasChanged = true;
}
else if (this._stepTimer >= this._stepPeriod)
{
if (this._rawValue >= 10)
{
//hit the upper edge, then begin to fall
this._rawValue = 10;
this._rawDirection = -1;
}
else if (this._rawValue <= -10)
{
//hit the lower edge, then begin to rise
this._rawValue = -10;
this._rawDirection = 1;
}
this._rawValue += this._rawDirection;
this.Value = this.Offset + (int)(this.Scale * (this._rawValue / 10.0));
hasChanged = true;
this._stepTimer = 0;
}
return (this.HasChanged = hasChanged);
}
// ...
}
三角波在示波器上的样子(100 Hz,仅供参考)。
当然,我也可以像上图快照那样,使用普通的台式波形发生器作为物理信号源。那样会更真实,但预期的波形周期会太短(即太快),并且“变化”以及随之而来的消息上传会过于频繁。基于软件的信号生成器非常适合非常长的周期,比如很多分钟。
HTTP 客户端
如上所述,数据通过普通的(未加密)HTTP 发送到服务器。Netduino Plus 2 不提供任何 HTTP 客户端,但提供了一些可用于创建自己的原始工具。
简单来说,客户端非常简单。如果您知道基本的 HTTP 事务是如何工作的,那么您将很容易理解代码的作用。
/// <summary>
/// HTTP Azure-mobile service client
/// </summary>
public class MobileServiceClient
{
public const string Read = "GET";
public const string Create = "POST";
public const string Update = "PATCH";
// ...
/// <summary>
/// Create a new client for HTTP Azure-mobile servicing
/// </summary>
/// <param name="serviceName">The name of the target service</param>
/// <param name="applicationId">The application ID</param>
/// <param name="masterKey">The access secret-key</param>
public MobileServiceClient(
string serviceName,
string applicationId,
string masterKey
)
{
this.ServiceName = serviceName;
this.ApplicationId = applicationId;
this.MasterKey = masterKey;
this._baseUri = "http://" + this.ServiceName + ".azure-mobile.net/";
}
// ..
private JToken OperateCore(
Uri uri,
string method,
JToken data
)
{
//create a HTTP request
using (var request = (HttpWebRequest)WebRequest.Create(uri))
{
//set-up headers
var headers = new WebHeaderCollection();
headers.Add("X-ZUMO-APPLICATION", this.ApplicationId);
headers.Add("X-ZUMO-MASTER", this.MasterKey);
request.Method = method;
request.Headers = headers;
request.Accept = JsonMimeType;
if (data != null)
{
//serialize the data to upload
string serialization = JsonHelpers.Serialize(data);
byte[] byteData = Encoding.UTF8.GetBytes(serialization);
request.ContentLength = byteData.Length;
request.ContentType = JsonMimeType;
request.UserAgent = "Micro Framework";
//Debug.Print(serialization);
using (Stream postStream = request.GetRequestStream())
{
postStream.Write(
byteData,
0,
byteData.Length
);
}
}
//wait for the response
using (var response = (HttpWebResponse)request.GetResponse())
using (var stream = response.GetResponseStream())
using (var reader = new StreamReader(stream))
{
//deserialize the received data
return JsonHelpers.Parse(
reader.ReadToEnd()
);
};
}
}
}
上面的代码源自一个旧的项目,但实际上只有该版本中的几行代码。然而,我想为对此感兴趣的人提供来源。
如Azure Mobile Services 所提供的,有两种类型的 API 可以调用:表(数据库)和自定义 API 操作。同样,我将在下一篇文章中详细介绍这些功能。
关键在于 OperateCore
方法,它是表和自定义 API 请求的私有入口点。所有 Azure 所需的是一些特殊的 HTTP 标头,其中应包含标识密钥以获得平台访问权限。
请求的内容只是一个 JSON 文档,也就是简单的纯文本。
主应用程序
程序启动时,它首先创建一个 Azure Mobile HTTP 客户端(Zumo)实例,然后将所有端口引用包装在一个数组中,以便于管理。
请注意,还有两个“特殊”端口称为“RampGenerator
”。在此演示中,有两个波形生成器,周期分别为 1200 秒和 1800 秒。它们的范围也略有不同,只是为了减少数据验证时的混淆。
能够将所有端口放入一个数组中,然后将它们视为一个统一的实体,这就是接口抽象所提供的优势。
public static void Main()
{
//istantiate a new Azure-mobile service client
var ms = new MobileServiceClient(
"(your service name)",
applicationId: "(your application-id)",
masterKey: "(your master key)"
);
//collect all the input ports as an array
var inputPorts = new IInput[]
{
_switch0,
_switch1,
new RampGenerator("Ramp20min", 1200, 100, 0),
new RampGenerator("Ramp30min", 1800, 150, 50),
_analog0,
_analog1,
};
初始化后,程序将永远循环运行,大约每秒采样所有端口。一旦出现任何“具体”变化,就会用新值构建一个 JSON 消息,然后发送到服务器。
//loops forever
while (true)
{
bool hasChanged = false;
//perform the logic sampling for every port of the array
for (int i = 0; i < inputPorts.Length; i++)
{
if (inputPorts[i].Sample())
{
hasChanged = true;
}
}
if (hasChanged)
{
//something has changed, so wrap up the data transaction
var jobj = new JObject();
jobj["devId"] = "01234567";
jobj["ver"] = 987654321;
var jdata = new JArray();
jobj["data"] = jdata;
//append only the port data which have been changed
for (int i = 0; i < inputPorts.Length; i++)
{
IInput port;
if ((port = inputPorts[i]).HasChanged)
{
port.Serialize(jdata);
}
}
//execute the query against the server
ms.ApiOperation(
"myapi",
MobileServiceClient.Create,
jobj
);
}
//invert the led status
_led.Write(
_led.Read() == false
);
//take a rest...
Thread.Sleep(1000);
}
JSON 消息的构成也许是最简单的部分,因为这是我的Micro-JSON 库的 Linq 方式。
LED 闪烁只是一个视觉心跳监视器。
消息架构
在我看来,应该不止一个板。更好的是:一个更真实的系统应该连接多个设备,即使它们彼此不同。然后,每个设备应该提供自己的数据,所有进入服务器的数据将构成一大堆“变量”。
因此,区分数据源很重要,一种“设备标识符”,在系统中是唯一的,将包含在每条消息中。
此外,我认为设备暴露的变量集可以随时更改。例如,我可能会添加一些新传感器,重新排列输入端口,甚至调整某些数据类型。所有这些都意味着“配置已更改”,服务器应该收到通知。这就是为什么还有“版本标识符”。
然后是实际的传感器数据。它只是一个 JavaScript 对象数组,每个对象提供端口(传感器)名称及其值。
但是,该数组将仅包含标记为“已更改”的端口。这个技巧至少有两个优点:
- 消息长度只包含有用数据
- 这种方法相当“松耦合”:服务器自动同步
每个变量的序列化由 IInput
接口中声明的相应方法完成。下面是一个模拟端口的示例。
public void Serialize(JArray container)
{
var jsens = new JObject();
jsens["name"] = this.Name;
jsens["value"] = this.Value;
container.Add(jsens);
}
这是初始消息,它始终包含所有值。
{
"devId": "01234567",
"ver": 987654321,
"data": [
{
"name": "Switch0",
"value": true
},
{
"name": "Switch1",
"value": true
},
{
"name": "Ramp20min",
"value": 0
},
{
"name": "Ramp30min",
"value": 50
},
{
"name": "Analog0",
"value": 0.073260073260073
},
{
"name": "Analog1",
"value": 45.079365079365
}
]
}
之后,我们可以调整微调电位器和开关以产生“变化”。一旦检测到任何变化,就会构建并发出一条消息。
单一变化 | 多次变化 |
---|---|
{
"devId": "01234567",
"ver": 987654321,
"data": [
{
"name": "Analog1",
"value": 52.503052503053
}
]
}
|
{
"devId": "01234567",
"ver": 987654321,
"data": [
{
"name": "Switch1",
"value": false
},
{
"name": "Analog1",
"value": 75.946275946276
}
]
}
|
结论
很容易认识到这个项目非常基础,而且有很多部分可以改进。例如,程序在抛出异常时没有任何恢复机制。但是,我想让应用程序保持非常入门级的水平。
是时候连接您自己的原型了,因为在下一篇文章中,我们将看到如何设置 Azure 平台进行数据处理。