Статьи

Клиент Microsoft Band для считывания температур и управления вентилятором на Raspberry PI2

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

Напомним, что функция Band-клиента заключается в следующем

  • Когда я нажимаю на плитку на полосе, пользовательский интерфейс открывается и показывает последние данные, а именно:

    • Температура как последнее измерение малиной PI2
    • Кнопка, которую я могу использовать для переключения вентилятора. Текст на кнопке отражает текущее состояние вентилятора (т. Е. Показывает «Остановить вентилятор», когда он работает, и «Запуск вентилятора», когда он не работает).
    • Дата и время последнего получения данных.

Занятые классы

Как вы можете видеть в  демонстрационном решении , есть три класса и один интерфейс, все они живут в пространстве имен Modelar TemperatureReader.ClientApp — которые имеют какое-то отношение к Band:

  • BandOperator
  • BandUiController
  • BandUiDefinitions
  • IBandOperator

Я немного  рассказал об этом в  предыдущем посте, в котором описывалось само приложение Windows 10 UWP, но резюмируя: приложение собрано с использованием внедрения зависимостей, так что каждый класс получает все (ну, почти все), что ему нужно, через конструктор и знает только то, что получает через интерфейс (не конкретный класс). Кроме того, большая часть общения происходит через события.

Начни это

Если вы посмотрите в метод CreateInstance MainViewModel, вы увидите, как BandOperator впервые вступает в жизнь:

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

Я выделил серым цветом то, что здесь не так важно, но вы видите, что BandOperator использует FanSwitchQueueClient ( см. Объяснение в этом посте ), чтобы отложить отправку данных на шину службы Azure, а затем они передаются в MainViewModel. По-видимому, для доставки данных о температуре с  сервисной шины Azure используется какой-то другой механизм  , и этот код находится в методе Start MainViewModel:

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

Вы можете видеть, что слушатель — будучи ITemperaListener, см. Также  этот пост  — просто передает событие BandOperator, запускает его и заставляет Band вибрировать. Итак, вы видите — реальный код, который управляет Band, очень слабо связан с остальной частью приложения. Но вы также видите, что Band не слушает данные, поступающие из служебной шины Azure, и не отправляет данные. Это  приложение  выполняет это взаимодействие, а Band — в свою очередь — взаимодействует с приложением.

Сначала некоторые определения

Важно понимать, что пользовательский интерфейс живет в группе, остается там. Его изменение и реагирование на события — это, по сути, процесс, который выполняется  на вашем телефоне , а не на Band, и вы взаимодействуете с процессом, который отправляет данные по Bluetooth, но в основном он абстрагирован. Создание пользовательского интерфейса группы также сильно отличается от того, к чему вы привыкли, используя XAML. Вы в основном пишете код для построения структуры пользовательского интерфейса, а затем заполняете его данными, используя еще больше кода. Нет таких вещей, как привязка данных. Все элементы пользовательского интерфейса должны быть определены с использованием уникальных идентификаторов — нет таких вещей, как легко узнаваемые имена. Он возвращается к прежним временам еще до Visual Basic.

В любом случае, есть отдельный класс определения, который содержит все необходимые идентификаторы:

using System;

namespace TemperatureReader.ClientApp.Models
{
  public static class BandUiDefinitions
  {
    public static readonly Guid TileId = 
      new Guid("567FF10C-E373-4AEC-85B4-EF30EE294174");
    public static readonly Guid Page1Id = 
      new Guid("7E494E17-B498-4610-A6A6-3D0C3AF20226");
    public static readonly Guid Page2Id = 
      new Guid("BB4EB700-A57B-4B8E-983B-72974A98D19E");

    public const short IconId = 1;
    public const short ButtonToggleFanId = 2;
    public const short TextTemperatureId = 3;
    public const short TextTimeId = 4;
    public const short TextDateId = 5;
  }
}

Таким образом, у нас есть три основных идентификатора пользовательского интерфейса — тайл и обе «страницы», которые должны иметь GUID. Я только что сгенерировал несколько с помощью Visual Studio, какой GUID вы используете на самом деле не имеет значения — они должны отличаться друг от друга и воздерживаться от их повторного использования — даже в проектах. Тогда есть пять элементов пользовательского интерфейса.

  • На первой странице — та, которую вы видите при нажатии на плитку: значок термометра, кнопка для включения или выключения вентилятора (также используется для отображения текущего состояния вентилятора) и метка, которая показывает температуру, измеренную с помощью Raspberry PI2
  • На второй странице два поля меток, которые показывают время и дату последнего полученного обновления от служебной шины Azure, полученного приложением.

Пользовательский интерфейс выглядит так:

IMG_2288Плитка, которую вы можете нажать

IMG_2289 
Первая страница с иконкой, показаниями температуры и кнопкой. Теперь на кнопке написано «Запустить вентилятор», поэтому, очевидно, приложение уже получило дату от Raspberry PI2, и это указывало на то, что вентилятор выключен. Обратите внимание, что небольшая часть второй страницы уже видна справа, предупреждая пользователя о том, что есть еще что-то, и побуждает его прокрутить вправо — шаблон пользовательского интерфейса, используемый с самых ранних дней Windows Phone 7.

IMG_2290Вторая страница (с датой и временем последних полученных данных)

Идентификаторы элементов пользовательского интерфейса   — это просто целые числа, но я делаю их глобально уникальными, то есть уникальными в приложении. Тот факт, что они распределены по двум «страницам», объясняется тем, что у Band очень маленький дисплей, и вам нужно будет использовать его многостраничные функции, если вы хотите показать что-либо, кроме самых тривиальных данных. К счастью, как только вы поймете, как это работает, это не очень сложно сделать.

Создание пользовательского интерфейса Band Band

Я отделил фактическое построение и управление пользовательским интерфейсом Band от «бизнес-логики», касающейся взаимодействия с событиями, поступающими из служебной шины Azure. Итак, у нас есть BandUiController и BandOperator. Грубая и не совсем правильная аналогия может определить BandUIController как представление, а BandOperator — как вид-вид-модель. Я сделал это, потому что однажды у меня был класс, приближающийся к 300 строкам, и все стало очень запутанным. Я разделил это. Я покажу лишь небольшую выдержку из BanOperator, прежде чем начать объяснять BandUIController.

BandUIController необходим доступ к IBandClient, чтобы иметь возможность работать с пользовательским интерфейсом Band. Вам нужно сначала найти один. Как это работает, вы можете увидеть в методе BandOperator GetNewBandClient:

var pairedBands = await BandClientManager.Instance.GetBandsAsync();
if (pairedBands != null && pairedBands.Any())
{
  return await BandClientManager.Instance.ConnectAsync(pairedBands.First());
}

И этот IBandClient вводится в BandUIController через конструктор. Мы видели эту модель раньше в этой серии

public class BandUiController
{
  private readonly IBandClient _bandClient;

  public BandUiController(IBandClient bandClient)
  {
    _bandClient = bandClient;
  }
}

Следующая важная вещь, которую нужно понять, состоит в том, что, хотя оба происходят из кода,  определение  интерфейса Band и фактическое отображение  содержимого в нем являются двумя отдельными действиями. Открытый интерфейс BandUIController на самом деле имеет только четыре метода — один из них перегружен другим:

public async Task<bool> BuildTile();
public async Task RemoveTile();
public async Task SetUiValues(string timeText, string dateText, 
                              string temperature, string buttonText);
public async Task SetUiValues(string temperature, string buttonText);

Первый создает плитку (и остальную часть пользовательского интерфейса), второй удаляет ее. Третий устанавливает все элементы пользовательского интерфейса значение , второй только что из элементов на первой странице — который используется для того, когда вы нажимаете на кнопку , чтобы выключить вентилятор включен или выключен. Итак, давайте сначала посмотрим на BuildTile.

public async Task<bool> BuildTile()
{
  if (_bandClient != null)
  {
    var cap = await _bandClient.TileManager.GetRemainingTileCapacityAsync();
    if (cap > 0)
    {
      var tile = new BandTile(BandUiDefinitions.TileId)
      {
        Name = "Temperature reader",
        TileIcon = await LoadIcon("ms-appx:///Assets/TileIconLarge.png"),
        SmallIcon = await LoadIcon("ms-appx:///Assets/TileIconSmall.png"),
      };

      foreach (var page in BuildTileUi())
      {
        tile.PageLayouts.Add(page);
      }
      await _bandClient.TileManager.AddTileAsync(tile);
      await _bandClient.TileManager.RemovePagesAsync(BandUiDefinitions.TileId);
      await _bandClient.TileManager.SetPagesAsync(BandUiDefinitions.TileId, 
        BuildIntialTileData());
      return true;
    }
  }
  return false;
}

Первое, что вам нужно сделать, это проверить оставшееся пространство на плитке. Есть только место для до 13 пользовательских плиток, так что скорее всего, там недостаточно места. Если места нет, этот клиент молча завершается с ошибкой. Но если есть место, плитка создается с назначенным GUID, большим и маленьким значком. Большой значок появляется на плитке, маленький значок обычно используется в уведомлениях, но оба могут быть использованы иначе (как мы увидим позже). «Большой», возможно, немного растягивает его, поскольку он только 46×46 (маленький — 24×24). «LoadIcon» — это небольшая подпрограмма, которая загружает иконки, и я выбрал их из  образцов Band . Затем страницы пользовательского интерфейса плитки создаются с использованием BuildTileUi и добавляются в коллекцию PageLayout плитки. Все идет нормально. Тогда все становится немного мутным.

  • Сначала мы добавляем плитку — с определениями страниц — в интерфейс Band. По умолчанию он добавляется в самый правый край полосы плитки — непосредственно перед настройкой зубчатой ​​передачи.
  • Затем мы удаляем любые возможные  данные,  возможно связанные со страницами, используя RemovePagesAsync. Помните, что это не  структура , а то, что  отображается на ней .  Я не уверен на 100%, что эта строка кода действительно нужна, но я просто оставил ее во время эксперимента
  • Затем мы добавляем данные по умолчанию для отображения на элементах пользовательского интерфейса страниц листов с помощью SetPagesAsync.

Давайте сначала посмотрим на BuildTileUi

private IEnumerable<PageLayout> BuildTileUi()
{
  var bandUi = new List<PageLayout>();

  var page1Elements = new List<PageElement>
  {
    new Icon {ElementId = BandUiDefinitions.IconId,       Rect = new PageRect(60,10,24,24)},
    new TextBlock  {ElementId = BandUiDefinitions.TextTemperatureId, 
      Rect = new PageRect(90, 10, 50, 40)},
    new TextButton {ElementId = BandUiDefinitions.ButtonToggleFanId, 
      Rect = new PageRect(10, 50, 220, 40), 
      HorizontalAlignment = HorizontalAlignment.Center}
  };

  var firstPanel = new FilledPanel(page1Elements) {     Rect = new PageRect(0, 0, 240, 150) };

  var page2Elements = new List<PageElement>
  {
    new TextBlock {ElementId = BandUiDefinitions.TextTimeId, 
      Rect = new PageRect(10, 10, 220, 40)},
    new TextBlock {ElementId = BandUiDefinitions.TextDateId, 
      Rect = new PageRect(10, 58, 220, 40)}
  };

  var secondPanel = new FilledPanel(page2Elements) {     Rect = new PageRect(0, 0, 240, 150) };

  bandUi.Add(new PageLayout(firstPanel));
  bandUi.Add(new PageLayout(secondPanel));

  return bandUi;
}

Теперь это может показаться немного пугающим, но на самом деле это не так сложно для чтения.

  • Сначала мы создаем элементы пользовательского интерфейса первой страницы — Icon, TextBlock и TextButton, все с местоположением и размером, определенными PageRect, относительно панели, в которой они будут находиться.
  • Затем мы создаем первую панель, добавляем на нее список элементов пользовательского интерфейса, созданных на предыдущем шаге, а затем определяем ее размер и местоположение с помощью PageRect. Я не совсем уверен, что максимальные значения для панели, но 240, 150 работает хорошо и оставляет достаточно места справа, чтобы сделать следующую страницу видимой
  • Затем мы создаем элементы пользовательского интерфейса второй панели — два текстовых блока одинакового размера, второй справа под первым
  • Затем мы создаем вторую панель того же размера, что и первая панель.
  • Наконец, мы создаем PageLayout из обеих панелей и добавляем их в список.

Как мы могли видеть в методе BuildTile, результатом метода BuildTileUi является добавление коллекции PageLayouts плитки:

foreach (var page in BuildTileUi()){
  tile.PageLayouts.Add(page);
}

На данный момент мы только определили  структуру  того, что должно отображаться на полосе. Он по-прежнему не отображает никаких данных.

Отображение данных в пользовательском интерфейсе группы

Давайте снова посмотрим на BuildTile. Он использует эту строку кода для отображения данных

 await _bandClient.TileManager.SetPagesAsync(BandUiDefinitions.TileId,          BuildIntialTileData());

BuildIntialTileData. это просто показывает некоторые строки по умолчанию, в свою очередь вызывает этот метод

private List<PageData> BuildTileData(string timeText, string dateText, 
                                     string temperature, string buttonText)
{
  var result = new List<PageData>
  {
    BuildTileDataPage2(timeText, dateText),
    BuildTileDataPage1(temperature, buttonText)
  };
  return result;
}

И затем мы подошли к сути вопроса (что касается отображения данных) — то есть, эти два маленьких метода:

private PageData BuildTileDataPage1(string temperature, string buttonText)
{
  return 
    new PageData(
      BandUiDefinitions.Page1Id, 0, 
      new IconData(BandUiDefinitions.IconId, 1),
      new TextButtonData(BandUiDefinitions.ButtonToggleFanId, buttonText),
      new TextBlockData(BandUiDefinitions.TextTemperatureId, $": {temperature}"));
}

private PageData BuildTileDataPage2(string timeText, string dateText)
{
  return 
    new PageData(BandUiDefinitions.Page2Id, 1,
      new TextBlockData(BandUiDefinitions.TextTimeId, $"Time: {timeText}"),
      new TextBlockData(BandUiDefinitions.TextDateId, $"Date: {dateText}"));
}

Давайте сначала рассмотрим BuildTileDataPage2, так как это наиболее просто для понимания. Это говорит, в основном: для страницы с Page2Id, которая является 2-й страницей в этом пользовательском интерфейсе (нумерация страниц начинается с нуля), установите текст в TextBlock с идентификатором BandUiDefinitions.TextTimeId и установите другой текст для BandUiDefinitions.TextDateId. Третий параметр конструктора PageData имеет тип  params PageElementData [],  так что вы можете просто добавить настройки значения пользовательского интерфейса в этот конструктор без необходимости определения списка.

В BuildTileDataPage1 мы делаем что-то похожее — за исключением того, что индекс страницы теперь равен 0 вместо 1, а текст в TextButton должен быть TextButtonData вместо TextBlockData. и первый элемент это IconData. Обратите внимание, что он добавляет значок с индексом 1. Это маленький значок. Помните этот кусок кода в BuildTile?

var tile = new BandTile(BandUiDefinitions.TileId)
{
  Name = "Temperature reader",
  TileIcon = await LoadIcon("ms-appx:///Assets/TileIconLarge.png"),
  SmallIcon = await LoadIcon("ms-appx:///Assets/TileIconSmall.png")
};

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

Теперь есть одна важная последняя информация, которую вы, возможно, не заметили. В BuildTileData я  сначала добавляю в список второй pagedata, а затем первый . Я посчитал необходимым сделать это таким образом, иначе пользовательский интерфейс отображается в обратном порядке (то есть страница с датой / временем отображается изначально, и вам приходится прокручивать страницу вбок с помощью кнопки и температуры. Иногда Просто иногда это происходит не так, как надо. Я не смог определить причину этого, но если вы добавляете pagedata в обратном порядке, это работает в большинстве случаев — как в случае, я видел, что он ошибался два или три раза, и только во время интенсивного развития.

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

public async Task SetUiValues(string timeText, string dateText, 
                              string temperature, string buttonText)
{
  var pageData = BuildTileData(timeText, dateText, temperature, buttonText);
  await _bandClient.TileManager.SetPagesAsync(BandUiDefinitions.TileId, pageData);
}

public async Task SetUiValues(string temperature, string buttonText)
{
  await
    _bandClient.TileManager.SetPagesAsync(BandUiDefinitions.TileId,
       BuildTileDataPage1(temperature, buttonText));
}

Первый обновляет весь пользовательский интерфейс, второй — только первую страницу. Вот что необходимо для создания пользовательского интерфейса и отображения на нем некоторых данных. Четыре элемента интерфейса, две страницы. Действительно заставляет вас ценить XAML, не так ли? 😉

Обработка группы взаимодействия

BandOperator в основном имеет только следующие функции:

  • Когда плитка нажата, покажите интерфейс Band с самыми последними данными, полученными с сервисной шины Azure.
  • При нажатии кнопки переключения отключите команду на служебной шине Azure.
  • Когда Raspberry PI2 подтвердит изменение статуса вентилятора, переключите текст на кнопке

… и тем не менее, это почти 250 строк. Многое из этого связано с проблемами, с которыми я столкнулся при возобновлении приостановленного приложения. Я попытался исправить это, используя довольно сложный метод для получения и создания клиента Band (метод GetBandClient) — и его вспомогательные методы занимают 60 строк. Поэтому я пропущу это, но посмотрим на это в  демонстрационном решении,  чтобы увидеть, как я пытался решить эту проблему. Я все еще не совсем удовлетворен этим, но это, кажется, работает. В большинстве случаев.

Переходя к методу Start BandOperator, вы можете увидеть, как настроено взаимодействие

public async Task<bool> Start(bool forceFreshClient = false)
{
  var tilePresent = false;
  var bandClient = await GetBandClient(forceFreshClient);
  if (bandClient != null)
  {
    var currentTiles = await bandClient.TileManager.GetTilesAsync();
    var temperatureTile = currentTiles.FirstOrDefault(
      p => p.TileId == BandUiDefinitions.TileId);
    if (temperatureTile == null)
    {
      var buc = new BandUiController(bandClient);
      tilePresent = await buc.BuildTile();
    }
    else
    {
      tilePresent = true;
    }

    if (tilePresent)
    {
      await bandClient.TileManager.StartReadingsAsync();
      bandClient.TileManager.TileOpened += TileManager_TileOpened;
      bandClient.TileManager.TileButtonPressed += TileManager_TileButtonPressed;
    }
  }
  IsRunning = tilePresent;
  return tilePresent;
}

По сути, этот метод пытается найти существующую плитку, и в случае неудачи создает BandUiController для ее создания. bandClient.TileManager.StartReadingsAsync затем активирует прослушивание событий Band — и, прикрепив события к TileOpened и TileButtonPressed, будут вызваны методы-обработчики — если нажата плитка на кнопке пользовательского интерфейса Band, или если кнопка на пользовательском интерфейсе плитки нажат.

private async void TileManager_TileOpened(object sender, 
  BandTileEventArgs<IBandTileOpenedEvent> e)
{
  var bandClient = await GetBandClient();
  if (bandClient != null)
  {
    if (e.TileEvent.TileId == BandUiDefinitions.TileId && _lastTemperatureData != null)
    {
      var buc = new BandUiController(bandClient);
      await buc.SetUiValues(
        _lastTemperatureData.Timestamp.ToLocalTime().ToString("HH:mm:ss"),
        _lastTemperatureData.Timestamp.ToLocalTime().ToString("dd-MM-yyyy"),
         $"{_lastTemperatureData.Temperature}°C",
         GetFanStatusText());
      await bandClient.NotificationManager.VibrateAsync(
         VibrationType.NotificationOneTone);
    }
  }
}

Так что самое забавное — этот метод вызывается, когда на полосе нажимают плитку. Любая  плитка, не обязательно только что созданная. Итак, сначала мы должны определить   , была ли нажата наша плитка, сравнив идентификатор плитки с идентификатором нашей плитки. В этом случае мы создаем BandUIController и обновляем значения пользовательского интерфейса последними полученными данными из шины службы Azure. И затем мы посылаем одну вибрацию, чтобы владелец Band знал, что новые данные были получены немедленно (без проверки даты и времени на 2-й странице нашего пользовательского интерфейса).

Аналогичная процедура применяется для обработки нажатия кнопки вентилятора:

private async void TileManager_TileButtonPressed(object sender, 
   BandTileEventArgs<IBandTileButtonPressedEvent> e)
{
  var te = e.TileEvent;
  if (te.TileId == BandUiDefinitions.TileId && 
    te.PageId == BandUiDefinitions.Page1Id && 
    te.ElementId == BandUiDefinitions.ButtonToggleFanId)
  {
    if (!_isProcessing)
    {
      _lastToggleUse = DateTime.UtcNow;
      _isProcessing = true;
      var cmd = new FanSwitchCommand(_lastFanStatus, true);
      Debug.WriteLine($"Sending fan command {cmd.Status}");
      await UpdateFirstPageStatus();
      await _fanStatusPoster.PostData(cmd);
    }
  }
}

Во-первых, нам нужно проверить, была ли нажата кнопка на нашей пользовательской компоновке — теоретически этого было бы достаточно, поскольку на ней есть только одна кнопка, но для хорошей меры вы также можете проверить страницу и идентификатор элемента в этом. стр. Затем происходит то, что текст кнопки изменяется на «Обработка», а FanSwitchCommand отправляется на Raspberry PI2.

Изменение текста на кнопке осуществляется через UpdateFirstPageStatus, который в свою очередь использует GetFanStatusText

private async Task UpdateFirstPageStatus()
{
  var bandClient = await GetBandClient();
  if (bandClient != null)
  {
    var text = GetFanStatusText();
    var buc = new BandUiController(bandClient);
    await buc.SetUiValues($"{_lastTemperatureData.Temperature}°C", text);
  }
}

private string GetFanStatusText()
{
  return _isProcessing ? "Processing" : 
    _lastTemperatureData.FanStatus == FanStatus.On ? "Stop fan" : "Start fan";
}

Логика этого заключается в следующем: _isProcessing используется, чтобы не дать пользователю нажимать кнопку переключения несколько раз подряд. Когда вы нажимаете кнопку, первое, что происходит, это то, что для _isProcessing установлено значение true, что фактически запрещает вам снова что-то делать с кнопкой. Текст на кнопке меняется на «Обработка». BandOperator теперь ждет, когда Raspberry PI2 подтвердит, что он действительно переключил вентилятор. Но вы не можете изменить значение  одного  элемента пользовательского интерфейса на странице Band — вам нужно обновить все из них. Поэтому я вызываю перегрузку SetUiValues ​​BandUiController с текстом новой кнопки  и  последней полученной температурой.

Итак, как замкнут цикл? Откуда BandOperator знает, что Raspberry PI2 действительно переключил вентилятор? Ответ лежит в методе HandleNewTength, который получает новые данные о температуре от остальной части приложения (помните, что он был подключен в MainViewModel.Start?)

public async void HandleNewTemperature(object sender, TemperatureData data)
{
  Debug.WriteLine(
    $"New temperature data received {data.Temperature} fanstatus = {data.FanStatus}");
  _lastTemperatureData = data;
  _lastTemperatureData.Timestamp = DateTimeOffset.UtcNow;
  if (_lastFanStatus != _lastTemperatureData.FanStatus && _isProcessing)
  {
    _isProcessing = false;
    _lastFanStatus = _lastTemperatureData.FanStatus;
    await UpdateFirstPageStatus();
  }
  else if (_lastToggleUse.IsSecondsAgo(Settings.FanSwitchQueueTtl) && _isProcessing)
  {
    _isProcessing = false;
    _lastFanStatus = _lastTemperatureData.FanStatus;
    await UpdateFirstPageStatus();
  }
  else if (!_isProcessing)
  {
    _lastFanStatus = _lastTemperatureData.FanStatus;
  }
}

Таким образом, полученные данные о температуре содержат не только температуру, но и текущее состояние вентилятора. Но если этот метод должен был принять статус вентилятора сразу после того, как мы отослали команду на переключение вентилятора, текст кнопки немедленно вернется к старому тексту — потому что команда еще не достигла Raspberry PI2, и это не успел среагировать и отправить подтверждение.

Итак, что мы делаем — когда поступают новые данные о температуре, _isProcessing имеет значение true (поэтому пользователь недавно нажал кнопку переключения),  а  полученные данные указывают на изменение состояния —  тогда  Raspberry PI2 получил команду переключения и воздействовал на нее. Таким образом, кнопка обновляется с «Обработка» до значения в соответствии с новым статусом вентилятора. Если  нет изменение статуса, но последнее использование кнопки переключения было более давно, чем время жизни сообщения FanSwitchQueue — мы предполагаем, что команда никогда не достигала Raspberry PI2, мы обновляем _lastFanStatus до старого состояния и соответственно обновляем кнопку. В любых других случаях, если пользователь недавно не нажимал кнопку, мы просто сохраняем последний статус вентилятора. Это не имеет ничего общего с Band или самим пользовательским интерфейсом — оно просто имеет дело с возможными задержками доставки сообщений (и возможными сообщениями, не полученными другой стороной).

Вывод

Создание пользовательского интерфейса Band выполнимо, но вам нужно уделять много внимания деталям, чтобы все было правильно. Это определенно более сложная задача, чем создание пользовательского интерфейса с использованием Blend, поскольку вам, по сути, необходимо постоянно видеть весь пользовательский интерфейс в уме — нет никакого способа визуально спроектировать его или даже сделать его видимым, если не запускать приложение и проверять результат на Band , Отладка требует много времени и энергии. Реагирование на события и взаимодействие кода с удаленными устройствами также сопряжено с определенными проблемами. И иногда что-то идет не так — но не всегда понятно, было ли это вызвано тем, что я что-то делал неправильно или не понимал мельчайшие детали API интерфейса пользователя Band, тот факт, что я использую его в  Windows 10 мобильных устройств (которые пока находятся в режиме предварительного просмотра) или ошибки в различных API-интерфейсах (Band или иным образом), которые я использую для сшивания вещей. На кровоточащем краю вы страдаете от боли, но вам также больше всего нравится.

И все же, потенциальные варианты использования довольно привлекательны и дают хорошее представление о том, как  выглядит (очень близкое) будущее кодирования устройств с Windows  CoreIoT . И у этого есть практические приборы также. Недавно я был в отпуске в Нойштадт-ан-дер-Вайнштрассе (где, среди прочего, этот пост был написан таким образом, местоположение образца не было полностью случайным :)) У меня была эта штуковина, работающая дома, — но я включила ее в свой кабинет дома и подключила его к мощному прожектору вместо вентилятора. У меня с собой были Lumia 1520 и Band — и хотя я физически находился в Германии, я смог включить свет дома (и получить подтверждение, что он был на самом деле включен или выключен), нажав кнопку на моей группе. Таким образом, мы надеемся, что убедительные потенциальные грабители дом-житель дома был на своем компьютере, и дом был занят. Не то чтобы в любом случае стоило воровать, но не весело возвращаться домой и искать разбитые окна и прочее. Если это имело какое-либо влияние — я не знаю, но наш дом остался один во время нашего отсутствия.

Что ж, это знаменует собой конец довольно эпической и довольно объемной серии постов в блоге. Я рекомендую вам в последний раз загрузить и проверить  демо-решение  — и создавать собственные вещи с помощью CoreIoT и / или Band. В наши дни даже Доктор Кто увлекается носимыми технологиями, и мы должны быть такими же: D