Статьи

Управляйте своим кондиционером с Raspberry Pi и группой Microsoft — часть 5

Название изображениявступление

Вы не можете развертывать приложения непосредственно в Microsoft Band, поэтому на устройстве, к которому оно подключено, всегда выполняется приложение, на котором фактически выполняется код. Обычно это телефон, но, поскольку это универсальное приложение для Windows , нет причин, по которым оно не может работать на ПК, как показано на скриншоте :). Тем не менее, я обнаружил, что, хотя вы можете подключить Band к ПК, он будет настаивать на подключении к приложению, прежде чем показывать пользовательский интерфейс, поэтому вы не сможете использовать его на ПК. Так что пока вы должны пользоваться телефоном.

В этом сообщении в блоге будет рассказано о настройке самого приложения, фактически исключая большинство вещей, связанных с Band, и основное внимание будет уделено настройке приложения с разумной составляющей.

Это приложение также использует внедрение зависимостей, как обсуждалось в моем посте о приложении на Raspberry PI2, но в этом полностью используется шаблон MVVM — точнее, MVVMLight , моим коллегой MVP (хотя я едва могу стоять в его тень)  Лоран Бюньон . Я использую его ViewModelBase и Messenger, а также SimpleIoC для некоторой инверсии управления и DispatcherHelper, чтобы помочь мне решить потенциальные проблемы с фоновыми процессами, влияющими на пользовательский интерфейс.

Основные настройки приложения

Приложение запускается (конечно) в App.xaml.cs, из которого я покажу небольшой отрывок:

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
}

Первые две строки означают, что когда что-то запрашивает реализацию IMessageDisplayer, отправьте ему тостер. Аналогичная вещь относится к IErrorLogger. Получить что-то очень просто, используя GetInstance — см. App_UnhandledException. Toaster — это простой класс для отображения всплывающего сообщения, ErrorLogger — это то, что я написал для регистрации ошибок в локальном хранилище — для длительных процессов. Обратите внимание также на использование Messenger в App_Resuming. Все это является частью осознания модели представления того, что ему нужно знать, что всегда делает прямую зависимость

Если вы используете DispatcherHelper MVVMLight, не забудьте инициализировать его (я всегда делаю по какой-то причине, к счастью, сообщение об ошибке достаточно ясно), а затем я инициализирую свою основную модель представления. Который, поскольку это простое приложение, является единственной моделью представления;)

Начальная загрузка ViewModel

Часть модели представления, которая обрабатывает запуск и инициализацию, такова:

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));
    }
  }
}

И здесь вы снова видите создание ряда независимых объектов, которые будут только слабо связаны. Я мог бы пойти дальше и позволить создавать их, зарегистрировав их в SimpleIoC, но конструктор также позволит мне «внедрить» эти объекты в модель представления. Во всяком случае, мы видим, как создаются:

  • FanSwitchQueueClient, который будет передавать команду для включения или выключения вентилятора на Raspberry PI2 через служебную шину Azure. Это подробно объяснялось во 2-м посте  этой серии. Теперь он находится в режиме отправки, в отличие от приложения на Raspberry PI2.

  • TemperatureListener прослушивает данные о температуре, поступающие от Raspberry PI2. Это тонкая оболочка для TemperatureQueueClient (также объясненная во втором посте  этой серии).

  • ErrorLogger, который я опишу в будущем сообщении в блоге.

  • MessageDisplayer — это реализация, представляющая собой тост, как уже упоминалось. 

  • BandOperator — класс, который обрабатывает все взаимодействия с Microsoft Band в отношении этого приложения. Это будет подробно рассмотрено в следующем сообщении в блоге. Обратите внимание, что в качестве параметра он принимает FanSwitchQueueClient — сам BandOperator будет обрабатывать отправку команды переключения вентилятора (и откладывать фактическое выполнение до FanSwitchQueueClient).

На данный момент достаточно знать, что BandOperator имеет очень ограниченный публичный интерфейс, который выглядит следующим образом:

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

По сути, это очень тонкая оболочка для TemperatureQueueClient, которая только добавляет «запоминание» состояния очереди к основным функциям:

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;
}

Обратите внимание, что он переводит TemperatureQueueClient в режим прослушивания — это снова именно зеркальное отображение того, что происходит на Raspberry PI2.

Открытый интерфейс модели представления, то есть то, что используется для привязки данных к этим 5 свойствам и одному методу, — это единственные вещи, которые доступны «внешнему миру» в отношении модели основного представления:

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); }
}

Метод «RemoveTile» вызывается для удаления пользовательской плитки из группы и привязывается к кнопке, помеченной как таковая. IsListening связан с тумблером, IsBusy связан с кольцом выполнения и полупрозрачным наложением, которое будет отображаться при переключении тумблера, а остальные свойства являются просто свойствами отображения.

Есть один вызов BandOperator — позже мы увидим больше. Открытый интерфейс для BandOperator очень ограничен, как и интерфейс для всех классов в этом проекте:

public interface IBandOperator
{
  Task<bool> Start(bool forceFreshClient = false);
  Task SendVibrate();
  Task Stop ();
  Task RemoveTile();
  void HandleNewTemperature(object sender, TemperatureData data);
  bool IsRunning { get; }
}

И это все, что вам нужно знать от BandOperator для этого сообщения в блоге.

MVVЛегкие поклонники вроде меня могут заметить, что наш хороший друг RelayCommand — MIA. Это потому, что в XAML я использую новый синтаксис x: Bind, как вы, возможно, видели в этой StackPanel в MainPage.xaml, которая показывает большую часть отображаемого текста:

<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>

Этот новый способ привязки позволяет напрямую связывать публичные методы viewmodel с событиями, происходящими в интерфейсе пользователя, поэтому нам больше не нужна команда:

<Button Grid.Row="5" Click="{x:Bind ViewModel.RemoveTile}"  
  Content="Remove tile from Band" HorizontalAlignment="Center"/>

Подробную информацию о том, как привязать события непосредственно к событиям, можно найти здесь . Чтобы иметь возможность использовать x: Bind, объект для привязки должен быть открытым свойством кода класса . Это вы можете увидеть в MainPage.xaml.cs:

public MainViewModel ViewModel
{
  get { return MainViewModel.Instance; }
}

Запуск и остановка

Как вы можете видеть из IsListening, должен быть метод Toggle, который запускается, когда установлено свойство IsListening. Он действительно есть, и он — и его друзья — реализованы так:

        

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";
    });
  }
}

Началов основном все это сбивает с толку. Я обнаружил, что если вы не укажете Task.Delay (1), настройка IsBusy не повлияет на пользовательский интерфейс. Однажды, и я буквально говорю о прошлом веке здесь, я использовал DoEvents () в Visual Basic (6, да), который имел точно такой же эффект;). Теперь вы видите кольцо прогресса и наложение на остальной части пользовательского интерфейса. И эта ViewModel, и бандуоператор предназначены для прослушивания входящих температурных событий на TemperatureListener, и затем запускается этот TemperatureListener. Bandoperator может делать с ним все, что захочет. Затем мы запускаем «фоновую сессию», чтобы приложение оставалось в живых как можно дольше. Затем запускается оператор диапазона — это фактически создает плитку и пользовательский интерфейс на подключенном диапазоне, если его там еще нет, и диапазон будет вибрировать.Приложение работает сейчас.

Наконец, в методе Viewer модели Listener_OnTemperaDataReceived данные помещаются на экран телефона и затем передаются в сообщении заинтересованным сторонам.

Stop , конечно, аккуратно отключает все события снова и останавливает все компоненты.


Обобщение потока событий : данные о температуре передаются следующим образом:

TemperatureQueueClient.OnDataReceived -> TemperatureListener.OnTeurationDataReceived ->

MainViewModel.Listener_OnTemperaDataReceived + Messenger + BandOperator.HandleNewTe Temperature

И команды для переключения потока вентилятора так:

BandOperator -> FanSwitchQueueClient.PostData

А остальное делается через привязку данных. Как именно работает BandOperator, заслуживает отдельная запись в блоге, которая закончит эту серию.

Сохраняя приложение живым

Если вы нажмете ToggleSwitch с меткой «Получить данные о температуре», вы заметите, что Windows 10 mobile попросит вас разрешить приложению отслеживать ваше местоположение . По сути, это хитрость, чтобы приложение оставалось живым как можно дольше — как я уже говорил, код для обеспечения работы пользовательского интерфейса Band работает на вашем телефоне, но только до тех пор, пока приложение работает (и не приостановлено). , Я использую ExtendedExecutionSession, чтобы обмануть ваш телефон и подумать, что это приложение отслеживает местоположение в фоновом режиме и должно оставаться в живых как можно дольше.

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();
}

Я думаю, что использование ExtendedExecutionSession было впервые описано моим коллегой MVP Мортеном Нильсеном в этой статье . Я также получил руководство по использованию от моего друга Маттео Пагани . В этой демонстрации я явно неправильно использую ExtendedExecutionSession, но это своего рода делает свое дело — приложение не сразу приостанавливается (как это происходит со многими обычными приложениями), но более или менее поддерживается, пока телефон не нуждается в ЦП и / или память и приостанавливает его в конце концов. Так что этот трюк только задерживает неизбежное, но для демонстрационных целей он достаточно хорош. Вероятно, лучший способ описан в этой статье Джеймсом Крофтом , который использует DeviceUseTrigger.

StartFakeGeolocator не делает ничего особенного, кроме создания Geolocator, который прослушивает изменения местоположения, но ничего не делает с ним. Посмотрите на источники в демонстрационном решении, если вы заинтересованы.

Приостановка и возобновление
Если, наконец, приходит запрос на приостановку, я аккуратно выключаю BandOperator, чтобы, если не получилось, всплыли все виды ошибок, связанных с доступом к уже расположенным нативным объектам. Но он также показывает сообщение (то есть тост), которое при касании может использоваться для простого перезапуска приложения, а затем запускается OnResume.

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}");
    }
  }
}

После возобновления мне нужно только перезапустить BandOperator снова (и поддельный Geolocator для хорошей меры).

BlinkBehavior

Как я уже показывал, TemperatureData также передается в MVVMLight Messenger при получении. Это по веским причинам — я хочу, чтобы круг посередине мигал акцентным цветом при получении данных. Это достигается путем прослушивания того самого сообщения:

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; }
  }
}
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; }
  }
}

Это не совсем ракетостроение: прослушайте DataReceivedMessage, и, если он получен, установите цвет прикрепленной фигуры (в данном случае круг) на акцентный цвет, а затем верните его исходному цвету. Эффект можно увидеть на видео в первом посте этой серии.

Вывод

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.