看过美剧《生活大爆炸》的朋友可能记得,里面有一个场景就是那群物理学家搞了个互联网控制的灯泡。从家里电脑上按下开关,信号绕地球一大群回来,点亮家里的台灯。虽然很屌丝,但是今天我成功在树莓派3上用Windows 10 IoT Core和Microsoft Azure实现了这个实验。
首先,Azure的参考文献有两篇, 强烈建议大家先阅读 ,本文不会再重复这两篇文章里的基础步骤:
Get started with Azure IoT Hub for .NET
https://azure.microsoft.com/en-us/documentation/articles/iot-hub-csharp-csharp-c2d/
创建Iot Hub的步骤参考 Get started with Azure IoT Hub for .NET 里的" Create an IoT Hub "章节,步骤完全一致。
注册设备我用了一个简单的方法,不用去做"Create a device identity"里的步骤。微软官方有个开源工具 https://github.com/Azure/azure-iot-sdks/tree/master/tools/DeviceExplorer
下载之后,到主界面填写连接字符串,然后点击 Update 按钮。
连接字符串在Azure Portal里有可以拿到。参考文章里也有说明。主要用到的就是 Hostname , SharedAccessKeyName , SharedAccessKey
HostName=你的HUB名称.azure-devices.net;SharedAccessKeyName=iothubowner;SharedAccessKey=你的KEY
然后在Management标签下面点击Create,填写你的设备名称,勾选"Auto Generate Keys"注册你的树莓派。
注册成功以后你应该能看到这个设备的信息被加入表格了,然后就可以进行下一步了。
需要材料:LED灯泡一只、杜邦线2根
LED | 树莓派 |
---|---|
长脚 | DC 3.3v |
短脚 | GPIO 04 |
注意,如果你用的LED额定电压不是3.3V的,请加上对应的电阻,以防烧掉。
连接完成看上去就像这样:
我们一共需要2个工程,第一个是树莓派上用的,这个是做接收端的,用来接受Azure发来的信息并控制LED的开关。另一个是电脑上用的,做“遥控器”,向Azure发送控制信息。
最后工程的结构应该长的是这样的:
用VS2015创建一个UWP工程,比如AzureRemoteLight,然后加入"Windows IoT Extensions for the UWP"的引用。
还需要添加的NuGet引用是:
"Microsoft.Azure.Devices.Client": "1.0.5"
同时建议大家把依赖项Newtonsoft.Json更新到最新版,目前是
"Newtonsoft.Json": "8.0.3"
我自己的这个工程还引用了MvvmLight和我自己的Edi.UWP.Helpers,当然这些不是必须的,只是用来装逼的。完整的project.json:
{ "dependencies": { "Edi.UWP.Helpers": "1.0.11", "Microsoft.Azure.Devices.Client": "1.0.5", "Microsoft.NETCore.UniversalWindowsPlatform": "5.0.0", "MvvmLight": "5.2.0", "Newtonsoft.Json": "8.0.3" }, "frameworks": { "uap10.0": {} }, "runtimes": { "win10-arm": {}, "win10-arm-aot": {}, "win10-x86": {}, "win10-x86-aot": {}, "win10-x64": {}, "win10-x64-aot": {} } }
其实对于单纯完成开灯关灯这个功能来说,界面不是必要的。当然,有界面的话,逼格更高一点。我画了一个这样的界面,看起来很牛逼:
主要就是2个地方:Azure IoT Hub Connection显示是否能成功连接到Azure(稍后会发一个message到azure更新这个label的状态)
CloudToDeviceLog显示从Azure接受到的控制信息。
完整XAML代码:
<Page x:Class="AzureRemoteLight.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:AzureRemoteLight" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" DataContext="{Binding Source={StaticResource Locator}, Path=Main}" mc:Ignorable="d"> <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Padding="12"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Image Source="Assets/cloud-hero.png" Grid.Row="0" Height="80" HorizontalAlignment="Right" VerticalAlignment="Top" /> <StackPanel Grid.Row="0"> <TextBlock Text="Windows 10 IoT + Microsoft Azure" Style="{StaticResource SubtitleTextBlockStyle}" /> <TextBlock Text="Remote Light Control" Style="{StaticResource SubheaderTextBlockStyle}" /> </StackPanel> <Grid Grid.Row="1" Margin="0,20,0,0"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Image Grid.Row="0" Grid.RowSpan="2" Source="Assets/Windows_Insiders_Flag.png" HorizontalAlignment="Right" Height="300" VerticalAlignment="Bottom" /> <StackPanel Orientation="Horizontal" Grid.Row="0"> <TextBlock Text="Azure IoT Hub Connection: " /> <TextBlock Text="{Binding IsAzureConnected}" /> </StackPanel> <Border Grid.Row="1" BorderBrush="#CCC" BorderThickness="1" Margin="0,10,0,0" Padding="10"> <TextBlock TextWrapping="Wrap" Text="{Binding CloudToDeviceLog}" Foreground="#CCC" FontFamily="Consolas" /> </Border> </Grid> </Grid> </Page>
首先,我们需要定义控制LED开关的两个对象,我的个人习惯是定义为属性,然后在执行的时候实例化。
#region GPIO Settings public GpioController GpioController { get; } public GpioPin LedPin { get; } #endregion
然后还有连接Azure IoT Hub的属性
#region Azure IoT Hub Settings public DeviceClient DeviceClient { get; } public string IotHubUri { get; } = "你的HUB名称.azure-devices.net"; public string DeviceKey { get; } = "设备对应的KEY"; public string DeviceId => "你的设备名称"; #endregion
这些属性值可以到Device Explorer里的Management标签下面去拿。
最后还有负责界面显示的两个属性
#region Display Fields private bool _isAzureConnected; private string _cloudToDeviceLog; public bool IsAzureConnected { get { return _isAzureConnected; } set { _isAzureConnected = value; RaisePropertyChanged(); } } public string CloudToDeviceLog { get { return _cloudToDeviceLog; } set { _cloudToDeviceLog = value; RaisePropertyChanged(); } } #endregion
然后在构造函数里初始化DeviceClient和GPIO端口
public MainViewModel() { DeviceClient = DeviceClient.Create(IotHubUri, new DeviceAuthenticationWithRegistrySymmetricKey(DeviceId, DeviceKey)); GpioController = GpioController.GetDefault(); if (null != GpioController) { LedPin = GpioController.OpenPin(4); LedPin.SetDriveMode(GpioPinDriveMode.Output); } }
还要写一个方法,向Azure发送一条信息,试试看连接是不是成功
public async Task SendDeviceToCloudMessagesAsync() { try { var telemetryDataPoint = new { deviceId = DeviceId, message = "Hello" }; var messageString = JsonConvert.SerializeObject(telemetryDataPoint); var message = new Message(Encoding.ASCII.GetBytes(messageString)); await DeviceClient.SendEventAsync(message); Debug.WriteLine("{0} > Sending message: {1}", DateTime.Now, messageString); IsAzureConnected = true; } catch (Exception ex) { Debug.WriteLine(ex.Message); } }
别忘了在MainPage.xaml.cs里调用一下:
public sealed partial class MainPage : Page { private MainViewModel _vm; public MainPage() { this.InitializeComponent(); _vm = this.DataContext as MainViewModel; Loaded += async (sender, args) => { // send device connected message await _vm.SendDeviceToCloudMessagesAsync(); }; }
首先要保证树莓派的系统时间是正确的,这步是必须的,不然SAS Token是要过期的。如果时间不对,重启几次设备应该就能同步正确。w32tm /resync /force这条明令是逗你们玩的,千万别相信它有用。
在运行之前,打开Device Explorer的Data标签,然后点击Monitor,以接受树莓派发送到Azure的信息。
然后用ARM, Remote Machine的配置到树莓派上去部署和执行,成功的话,Device Explorer里会接受到这样的信息:
然后我们就能继续爆代码了。
在ViewModel里再加个方法,用来接收Azure发过来的信息,然后根据信息内容向LED输出高低电平实现控制灯泡开关:
public async Task ReceiveCloudToDeviceMessageAsync() { CloudToDeviceLog = "Receiving events..."; Debug.WriteLine("/nReceiving cloud to device messages from service"); while (true) { Message receivedMessage = await DeviceClient.ReceiveAsync(); if (receivedMessage == null) continue; var msg = Encoding.ASCII.GetString(receivedMessage.GetBytes()); CloudToDeviceLog += "/nReceived message: " + msg; if (msg == "on") { LedPin.Write(GpioPinValue.Low); } if (msg == "off") { LedPin.Write(GpioPinValue.High); } await DeviceClient.CompleteAsync(receivedMessage); } }
消息用的是string类型,如果内容是"on"就开灯,如果是"off"就关灯。
当然,也要在MainPage.xaml.cs里再调用一下这个方法
public sealed partial class MainPage : Page { private MainViewModel _vm; public MainPage() { this.InitializeComponent(); _vm = this.DataContext as MainViewModel; Loaded += async (sender, args) => { // send device connected message await _vm.SendDeviceToCloudMessagesAsync(); // receive remote light control events await _vm.ReceiveCloudToDeviceMessageAsync(); }; } }
现在再执行,就应该能在树莓派上看到这样的画面:
至此,树莓派端工作就完成了。
创建一个WPF工程,比如LightController,不能是UWP(马上解释)。然后添加Microsoft.Azure.Devices的NuGet包。这个包不支持UWP,所以建不出UWP的控制端,Stupid!
然后在MainWindow里画两个按钮,分别用来发送开灯的消息和关灯的消息:
<Window x:Class="LightController.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:LightController" mc:Ignorable="d" Title="MainWindow" Height="350" Width="525"> <Grid> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <Button x:Name="BtnTurnOn" Content="Turn Light On" HorizontalAlignment="Center" Click="BtnTurnOn_OnClick" /> <Button x:Name="BtnTurnOff" Content="Turn Light Off" Margin="0,10,0,0" Click="BtnTurnOff_OnClick" /> </StackPanel> </Grid> </Window>
后台代码:
public partial class MainWindow : Window { static ServiceClient serviceClient; static string connectionString = "你的IOT HUB连接字符串"; public MainWindow() { InitializeComponent(); serviceClient = ServiceClient.CreateFromConnectionString(connectionString); } private async Task TurnLight(bool isOn) { await SendCloudToDeviceMessageAsync(isOn); } private static async Task SendCloudToDeviceMessageAsync(bool isOn) { var commandMessage = new Message(Encoding.ASCII.GetBytes(isOn ? "on" : "off")); await serviceClient.SendAsync("你的设备名称", commandMessage); } private async void BtnTurnOn_OnClick(object sender, RoutedEventArgs e) { await TurnLight(true); } private async void BtnTurnOff_OnClick(object sender, RoutedEventArgs e) { await TurnLight(false); } }
这里发送的消息也是string,和树莓派端的匹配,on表示开灯,off表示关灯。
IOT HUB的连接字符串就是Device Explorer里的Configruation标签里用的连接字符串,它们是一模一样的!
因为有控制端和接收端,所以我们要同时启动2个工程。在VS里,对solution名称点右键,选择属性,然后这样搞:
启动后,在电脑上点击WPF控制端里的两个按钮就能控制LED开关了,并且在树莓派的屏幕上也能显示接受的控制信息:
最后,完整代码在这里:
https://github.com/EdiWang/Windows-IoT-AzureRemoteLight