Part 5 of Reading temperatures & controlling a fan with a RP2, Azure Service Bus and a Microsoft Band
Intro
You cannot deploy apps to a Microsoft Band directly, so there is always a kind of app running on the device to which it is paired on which the code is actually running. Typically this is a phone, but there since this is a Universal Windows App, there is no reason why it could not run on a PC, like this screenshot shows :). Yet, I have found out that although you can pair a Band to a PC, it will insist on connecting to the app before showing a UI, so you cannot actually use it with a PC. So for the time being, you should use a phone.
This blog post will talk about the setup of the app itself, actually excluding most of the stuff related to the Band - and concentrate on how to setup a reasonably componentized app.
This app also uses dependency injection, as discussed in my post about the app on the Raspberry PI2, but this one makes full use of the MVVM pattern - to be more specific, MVVMLight, by my fellow MVP (although I can barely stand in his shade) Laurent Bugnion. I make use of his ViewModelBase and Messenger, as well as SimpleIoC for some inversion of control, and DispatcherHelper to get help me solve potential issues with background processes having effects on the UI.
Basic app setup
The app starts (of course) in App.xaml.cs, of which I show a little excerpt:
public App() { SimpleIoc.Default.Register<IMessageDisplayer, Toaster>(); SimpleIoc.Default.Register<IErrorLogger, ErrorLogger>(); InitializeComponent(); Suspending += OnSuspending; UnhandledException += SimpleIoc.Default. GetInstance<IErrorLogger>().LogUnhandledException; UnhandledException += App_UnhandledException; Resuming += App_Resuming; } private void App_Resuming(object sender, object e) { Messenger.Default.Send(new ResumeMessage()); } private void App_UnhandledException(object sender, UnhandledExceptionEventArgs e) { SimpleIoc.Default.GetInstance<IMessageDisplayer>(). ShowMessage ($"Crashed: {e.Message}"); } protected override void OnLaunched(LaunchActivatedEventArgs e) { DispatcherHelper.Initialize(); MainViewModel.Instance.Init(); // stuff omitted }
The first two lines mean that whenever something requests something implementing an IMessageDisplayer, send him a Toaster. A similar thing goes for IErrorLogger. Retrieving something is a easy as using GetInstance - see App_UnhandledException. Toaster is a simple class to show a toast message, ErrorLogger is something I wrote for logging errors in local storage - for long running processes. Notice also the use of the Messenger in the App_Resuming. This is all part of making the viewmodel aware of things it needs to know, which ever making a direct dependency
If you use MVVMLight's DispatcherHelper, don't forget to initialize it (I always do for some reason, fortunately the error message is clear enough) and then I initialize my main viewmodel. Which, since this is a simple app, is the only viewmodel as well ;)
Bootstrapping the viewmodel
The part of the viewmodel that handles startup and initializing is this:
using System; using System.Globalization; using System.Threading.Tasks; using Windows.ApplicationModel.ExtendedExecution; using Windows.Devices.Geolocation; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Ioc; using GalaSoft.MvvmLight.Messaging; using GalaSoft.MvvmLight.Threading; using TemperatureReader.ClientApp.Helpers; using TemperatureReader.ClientApp.Messages; using TemperatureReader.ClientApp.Models; using TemperatureReader.ServiceBus; namespace TemperatureReader.ClientApp.ViewModels { public class MainViewModel : ViewModelBase { private readonly ITemperatureListener _listener; private readonly IBandOperator _bandOperator; private readonly IMessageDisplayer _messageDisplayer; private readonly IErrorLogger _errorLogger; public MainViewModel(ITemperatureListener listener, IBandOperator bandOperator, IMessageDisplayer messageDisplayer, IErrorLogger errorLogger) { _listener = listener; _bandOperator = bandOperator; _messageDisplayer = messageDisplayer; _errorLogger = errorLogger; } public void Init() { Messenger.Default.Register<ResumeMessage>(this, async msg => await OnResume()); } // lots omitted private static MainViewModel _instance; public static MainViewModel Instance { get { return _instance ?? (_instance = CreateNew()); } set { _instance = value; } } public static MainViewModel CreateNew() { var fanStatusPoster = new FanSwitchQueueClient(QueueMode.Send); fanStatusPoster.Start(); var listener = new TemperatureListener(); var errorLogger = SimpleIoc.Default.GetInstance<IErrorLogger>(); var messageDisplayer = SimpleIoc.Default.GetInstance<IMessageDisplayer>(); var bandOperator = new BandOperator(fanStatusPoster); return (new MainViewModel( listener, bandOperator, messageDisplayer, errorLogger)); } } }And here you see once again creation of a number of independent objects that will only be loosely connected. I could have gone further and let these also be created by having them registered in SimpleIoC, but the constructor will also allow me to 'inject' these objects into the viewmodel. Anyway, we see being created:
- a FanSwitchQueueClient, that will transport the command to switch the fan on or off to the Raspberry PI2 via the Azure Service bus. It's details were explained in the 2nd post of this series. It's now in Send mode, as opposed to the app on the Raspberry PI2.
- TemperatureListener listens to temperature data coming from the Raspberry PI2. It's a thin wrapper around TemperatureQueueClient (also explained in in the 2nd post of this series)
- An ErrorLogger, which I will describe in a future blog post
- A MessageDisplayer - it's implementation being something that shows a toast, as already mentioned
- A BandOperator - a class that handles all interactions with the Microsoft Band as far as this app is concerned. This will be handled in detail in the next blog post. Notice it takes a FanSwitchQueueClient as a parameter - the BandOperator itself will handle the posting of the fan switch command (and defer the actual execution to the FanSwitchQueueClient).
For now, it's good enough to know the BandOperator has a very limited public interface, that looks like this:
public interface IBandOperator { Task<bool> Start(bool forceFreshClient = false); Task SendVibrate(); Task Stop (); Task RemoveTile(); void HandleNewTemperature(object sender, TemperatureData data); bool IsRunning { get; } }
TemperatureListener
This is basically a very thin wrapper around the TemperatureQueueClient, that only adds 'remembering' the queue's status to the basic functionality:
public class TemperatureListener : ITemperatureListener { private readonly TemperatureQueueClient _client; public TemperatureListener() { _client = new TemperatureQueueClient(QueueMode.Listen); _client.OnDataReceived += ProcessTemperatureData; } private void ProcessTemperatureData(object sender, TemperatureData temperatureData) { OnTemperatureDataReceived?.Invoke(this, temperatureData); } public async Task Start() { await _client.Start(); IsRunning = true; } public bool IsRunning { get; private set; } public void Stop() { _client.Stop(); IsRunning = false; } public event EventHandler<TemperatureData> OnTemperatureDataReceived; }Note that it puts the TemperatureQueueClient in Listen mode - this is again exactly the mirror image from what is happening on the Raspberry PI2
The view model's public interface - aka what is used for data binding
These 5 properties - and the one method - are the only things that are available to the 'outside world' as far as the main view model is concerned:
public async Task RemoveTile() { IsBusy = true; await Task.Delay(1); await _bandOperator.RemoveTile(); IsBusy = false; } public bool IsListening { get { return _listener?.IsRunning ?? false; } set { if (_listener != null) { if (value != _listener.IsRunning) { Toggle(); } } } } private string _temperature = "--.-"; public string Temperature { get { return _temperature; } set { Set(() => Temperature, ref _temperature, value); } } private string _lastDateTimeReceived = "--:--:-- ----------"; public string LastDateTimeReceived { get { return _lastDateTimeReceived; } set { Set(() => LastDateTimeReceived, ref _lastDateTimeReceived, value); } } private string _fanStatus = "???"; public string FanStatus { get { return _fanStatus; } set { Set(() => FanStatus, ref _fanStatus, value); } } private bool _isBusy; public bool IsBusy { get { return _isBusy; } set { Set(() => IsBusy, ref _isBusy, value); } }
The method "RemoveTile" is called to remove the custom tile from the Band and is bound to the button labeled as such. IsListening is bound to the toggle switch, IsBusy is bound to the progress ring and the semi-transparent overlay that will appear while you switch the toggle, and the rest of the properties are just display properties.
There is a single call to the BandOperator - later we will see more. The public interface for a BandOperator is very limited, as are the interface for all the classes in this project:
public interface IBandOperator { Task<bool> Start(bool forceFreshClient = false); Task SendVibrate(); Task Stop (); Task RemoveTile(); void HandleNewTemperature(object sender, TemperatureData data); bool IsRunning { get; } }
And that is all you need to know from the BandOperator for this blog post.
MVVMLight aficionados like me might notice that our good friend RelayCommand is MIA. This is because in the XAML I use the new x:Bind syntax, as you might have seen in this StackPanel in MainPage.xaml that show most of the text being displayed:
<StackPanel Grid.Row="2" Margin="0,0,0,16" Orientation="Vertical"> <TextBlock HorizontalAlignment="Center" FontSize="30" Margin="0" > <Run Text="{x:Bind ViewModel.Temperature, FallbackValue=--.-, Mode=OneWay}" /> <Run Text="°C" /> </TextBlock> <TextBlock HorizontalAlignment="Center" FontSize="15" Margin="0" > <Run Text="Fan is" /> <Run Text="{x:Bind ViewModel.FanStatus, FallbackValue=--.-, Mode=OneWay}" /> </TextBlock> <TextBlock Text="{x:Bind ViewModel.LastDateTimeReceived, FallbackValue=--:--:-- ----------, Mode=OneWay}" FontSize="10" HorizontalAlignment="Center"></TextBlock> </StackPanel>This new way of binding allows to directly binding public viewmodel methods to events happening in the user interfaced, so we don't need a command anymore:
<Button Grid.Row="5" Click="{x:Bind ViewModel.RemoveTile}" Content="Remove tile from Band" HorizontalAlignment="Center"/>
Detailed information on how to bind events directly to events can be found here. In order to be able to use x:Bind, the object to bind to needs to be a public property of the code behind class. This you can see in MainPage.xaml.cs:
public MainViewModel ViewModel { get { return MainViewModel.Instance; } }
Starting and stopping
As you can see from IsListening, there should be a Toggle method that is kicked off when the IsListening property is set. There is one indeed, and it - and it's friends - are implemented like this:
private async Task Toggle() { if (_listener.IsRunning) { await Stop(); } else { await Start(); } RaisePropertyChanged(() => IsListening); } private async Task Start() { IsBusy = true; await Task.Delay(1); _listener.OnTemperatureDataReceived += Listener_OnTemperatureDataReceived; _listener.OnTemperatureDataReceived += _bandOperator.HandleNewTemperature; await _listener.Start(); await StartBackgroundSession(); await _bandOperator.Start(); await _bandOperator.SendVibrate(); IsBusy = false; } private async Task Stop() { IsBusy = true; await Task.Delay(1); _listener.OnTemperatureDataReceived -= Listener_OnTemperatureDataReceived; _listener.OnTemperatureDataReceived -= _bandOperator.HandleNewTemperature; _listener.Stop(); await _bandOperator.Stop(); _session.Dispose(); _session = null; IsBusy = false; } private void Listener_OnTemperatureDataReceived(object sender, Shared.TemperatureData e) { if (e.IsValid) { DispatcherHelper.CheckBeginInvokeOnUI(() => { Messenger.Default.Send(new DataReceivedMessage()); Temperature = e.Temperature.ToString(CultureInfo.InvariantCulture); LastDateTimeReceived = e.Timestamp.ToLocalTime().ToString("HH:mm:ss dd-MM-yyyy"); FanStatus = e.FanStatus == Shared.FanStatus.On ? "on" : "off"; }); } }
Start basically kicks the whole thing off. I have found out that unless you specifiy the Task.Delay(1), setting IsBusy does not have any effect on the UI. Once, and I am literally talking the previous century here, I used DoEvents() in Visual Basic (6, yes) that had exactly the same effect ;) - now you get to see the progress ring and the overlay on the rest of the UI. Both this viewmodel and the bandoperator are made to listen to incoming temperature events on the TemperatureListener, and that TemperatureListener is started then. The bandoperator can do with it whatever it wants. Then we start a 'background session' to keep the app alive as long as possible. Then the band operator is started - this will in effect create a tile and a user interface on the connected Band, if that is not already there, and the Band will be made to vibrate. The application is running now.
Finally, in the viewmodel's Listener_OnTemperatureDataReceived method the data is put on the phone's screen and then passed around in a message to interested parties
Stop, of course, neatly disconnects all events again and stops all the components.
Flow of events
Summarizing: temperature data flows like this:
TemperatureQueueClient.OnDataReceived -> TemperatureListener.OnTemperatureDataReceived ->
MainViewModel.Listener_OnTemperatureDataReceived + Messenger + BandOperator.HandleNewTemperature
And commands to switch of the fan flow like this:
BandOperator -> FanSwitchQueueClient.PostData
And the rest is done via data binding. How the BandOperator exactly works merits a separate blog post, that will end this series.
Keeping the app alive
If you hit the ToggleSwitch that is labeled "Get temperature data" you will notice Windows 10 mobile asks you to allow the app to track your location. This is in essence a trick to keep the app alive as long as possible - as I said before, the code to make the Band UI work runs on your phone but only does so to as long as the app is running ( and not suspended). I use ExtendedExecutionSession to trick your phone to think this app is tracking location in the background and should be kept alive as long as possible.
private ExtendedExecutionSession _session; private async Task<bool> StartBackgroundSession() { if (_session != null) { try { _session.Dispose(); } catch (Exception){} } _session = null; { _session = new ExtendedExecutionSession { Description = "Temperature tracking", Reason = ExtendedExecutionReason.LocationTracking }; StartFakeGeoLocator(); _session.Revoked += async (p, q) => { await OnRevoke(); }; var result = await _session.RequestExtensionAsync(); return result != ExtendedExecutionResult.Denied; } return false; } private async Task OnRevoke() { await StartBackgroundSession(); }
I think using ExtendedExecutionSession was first described by my fellow MVP Morten Nielsen in this article. I also got some usage guidance on this from my friend Matteo Pagani. In this demo I am clearly misusing ExtendedExecutionSession, yet it kind of does the trick - the app is not suspended right away (as happens with a lot of normal apps) but is more or less kept alive, until the phone needs the CPU and/or memory and suspends it after all. So this trick only delays the inevitable, but for demo purposes it is good enough. A probably better way is described in this article by James Croft, which uses a DeviceUseTrigger.
The StartFakeGeolocator does nothing special but creating a Geolocator that listens to location changes but does nothing with it. Have a look at the sources in the demo solution if you are interested.
Suspend and resume
If the suspend request then finally comes, I neatly shut down the BandOperator for if I don't, all kinds of errors regarding accessing of already disposed native objects pop up. But it also shows a message (that is, a toast) that, when tapped, can be used to easily restart the app again and then OnResume kicks in.
public async Task OnSuspend() { if (_bandOperator != null && _bandOperator.IsRunning) { await _bandOperator.Stop(); await _messageDisplayer.ShowMessage("Suspended"); } } public async Task OnResume() { if ( IsListening && _bandOperator != null) { try { IsBusy = true; await Task.Delay(1); await StartBackgroundSession(); await _bandOperator.Start(true); await _bandOperator.SendVibrate(); IsBusy = false; } catch (Exception ex) { await _errorLogger.LogException(ex); await _messageDisplayer.ShowMessage($"Error restarting Band {ex.Message}"); } } }
Upon resuming , I only need to restart BandOperator again (and a fake Geolocator for good measure).
BlinkBehavior
As I already showed, TemperatureData is also broadcasted on the MVVMLight Messenger when it is received. This is for good reasons - I want the circle in the middle blink up in accent color when data is received. That is accomplished by a behavior listening to that very message:
using System.Threading.Tasks; using Windows.UI.Xaml; using Windows.UI.Xaml.Media; using Windows.UI.Xaml.Shapes; using GalaSoft.MvvmLight.Messaging; using Microsoft.Xaml.Interactivity; using TemperatureReader.ClientApp.Messages; namespace TemperatureReader.ClientApp.Behaviors { public class BlinkBehavior : DependencyObject, IBehavior { private Shape _shape; private Brush _originalFillBrush; private readonly Brush _blinkBrush = Application.Current.Resources["SystemControlHighlightAccentBrush"] as SolidColorBrush; public void Attach(DependencyObject associatedObject) { AssociatedObject = associatedObject; _shape = associatedObject as Shape; if (_shape != null) { _originalFillBrush = _shape.Fill; Messenger.Default.Register<DataReceivedMessage>(this, OnDateReceivedMessage); } } private async void OnDateReceivedMessage(DataReceivedMessage mes) { _shape.Fill = _blinkBrush; await Task.Delay(500); _shape.Fill = _originalFillBrush; } public void Detach() { Messenger.Default.Unregister(this); if (_shape != null) { _shape.Fill = _originalFillBrush; } } public DependencyObject AssociatedObject { get; private set; } } }
It is not quite rocket science: listen to the DataReceivedMessage, and if one is received, set the color of the attached Shape (a circle in this case) to the accent color, then return it to it's original color. The effect can be seen in the video in the first post of this series.
Conclusion
Quite a lot going on in this app, and then we haven't even seen what is going on with the Band. Yet, but using MVVMLight and neatly seperated components, you can easily wire together complex actions using simple patterns using interfaces and events. In the final episode of the series I will show you in detail how the Band interface is made and operated. In the mean time, have a look at the demo solution
2 comments:
Hi
I have a problem receiving data from Azure IOT Hub Using UWP app because of azure service bus is not supported
I tried the nuget < AzureSBLite > and it works only with notification and the UI cannot be loaded
any help please ??
@Mohamed, have you tried the ConnectTheDots sample? That was a good starting point for me! https://github.com/Azure/connectthedots
Post a Comment