Блог Дмитрия Сошникова

про технологии и человеческое счастье

Облачная погодная станция и интернет вещей

Наверняка вы слышали термин “интернет вещей” – про него сейчас много говорят. Речь идет о том, что количество подключенных к интернет устройств растёт экспоненциально, и скоро все вещи, окружающие нас, будут подключены к интернет и смогут взаимодействовать как с нами, так и между собой. Новогодние каникулы – отличное время для экспериментов, и мне захотелось немного поэкспериментировать с интернетом вещей и сделать свою погодную станцию. А заодно и сервис, который мог бы сравнивать данные о погоде “за окном” со значениями прогноза погоды, а в будущем возможно и делать какие-то предсказания на основе шаманского анализа временных рядов. Хочу поделиться с вами результатами.

Сделать свою погодную станцию – это традиционное развлечение, например оно описано здесь, здесь, здесь, и еще отчасти тут. Мне же хотелось построить всё на технологиях Майкрософт, при этом сделав упор на облачный сервис, который будет выполнять центральную роль во всей системе – а собственно сама станция будет просто отправлять данные в интернет. Посмотреть температуру за окном можно будет с мобильного устройства или через веб-интерфейс.

IMG_6268 image
image IMG_6263

Две составляющие интернета вещей

На сегодняшний день, интернет вещей становится возможным благодаря наличию двух важных составляющих:

  • Компактные клиентские устройства, подключенные к сети. С этой стороны процесс бурно развивается благодаря наличию недорогих микроконтроллеров, начиная от Arduino (который можно легко программировать на Visual Studio), и заканчивая экзотическими контроллерами, программируемыми на JavaScript.
  • Развитие облачных технологий, которые позволяют накапливать в облаке большие объемы данных и производить их анализ и обработку. С этой точки зрения очень интересно использование AzureML.

image

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

Архитектура облачной погодной станции

В соответствии с таким пониманием интернета вещей мы и спроектируем нашу погодную станцию. Роль конечного устройства будет выполнять датчик температуры и освещенности (зачем нужно мерить освещенность – я расскажу в отдельном посте), подключенный к интернет с помощью замечательного контроллера NetDuino Plus 2, а облачный сервис будет реализован на Microsoft Azure. Также мы будем получать сведения о текущей температуре из погодного сервиса OpenWeatherMap, чтобы сравнивать наши измерения с данным прогноза погоды.

image

Архитектура облачного сервиса

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

  • Хранилище данных. Нам важно сразу выбрать правильную архитектуру для хранения данных: полноценная база данных SQL, полуструктурированные Azure Storage или же решение типа NoSQL. В нашем случае проще всего было бы использовать SQL Azure, поскольку объем данных не очень велик – но мы предпочтем в образовательных целях использовать Azure Table Storage, истинное облачное хранилище, которое может потенциально сильно масштабироваться. Ценой такого решения будет то, что мы не сможем использовать достаточно богатые конструкции запросов к данным, что немного усложнит реализацию.
  • Веб-интерфейс, который будет отображать пользователю графики температуры, давления, освещенности и т.д. Я буду использовать ASP.NET Web Forms как самую простую технологию, однако на практике для повышения производительности конечно стоит рассмотреть ASP.NET MVC.
  • Веб-сервис для регистрации температуры, а также для передачи данных мобильному приложению. Используем замечательную технологию Web API, которую можно развернуть параллельно с веб-интерфейсом на ASP.NET (ниже расскажем, как).
  • Постоянно работающий процесс Worker Role, которые будет периодически собирать данные из сервиса OpenWeatherMap и сохранять в нашем хранилище.

Создание проекта в Visual Studio

Для начала создадим проект в Visual Studio (я буду использовать Visual Studio 2015). Если вы не хотите создавать проект сами, а лишь поэкспериментировать, то используйте готовый код проекта, доступный на GitHub: версию, описанную в статье, или последнюю версию.

Начнем с создания облачного сервиса, которые будет содержать в себе Web Role и Worker Role. Для работы с облачными проектами и инструментами вам потребуется установить Windows Azure SDK.

image

При создании облачной службы мы сразу добавляем в неё необходимые компоненты:

 image

При создании веб-составляющей WeatherMonitorWeb нас попросят выбрать стек технологий ASP.NET – в нашем случае я использовал ASP.NET Web Forms. Если кто-то поможет переписать этот проект на ASP.NET MVC – я буду признателен! Используйте pull request для исходного кода проекта на GitHub.

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

Организация доступа к данным

Данные о погоде будут хранится в табличном хранилище Microsoft Azure. Мы можем использовать встроенное хранилище, создаваемое в рамках облачной службы, или настроить отдельное хранилище, доступ к которому будет осуществляться из разных служб. Создадим отдельное хранилище – для этого в портале Microsoft Azure выберем следующий пункт:

image

При создании хранилища стоит использовать разумные опции (в зависимости от требований к надежности – выбирать реплицированное или локальное хранилище). После создания службы данных (мы назовём её weathermon) в меню “управление ключами” (внизу страницы) будут доступны ключи, которые потребуются нам позднее для доступа к данным.

image

Табличное хранилище позволяет хранить множество пар атрибут-значение на множестве распределенных серверов. Для удобства мы будем использовать Azure Storage SDK, который входит в состав Azure SDK. Необходимо добавить в проект ссылку (Reference) на WindowsAzure.Storage, либо использовать NuGet (install-package WindowsAzure.Storage).

Storage SDK позволит нам автоматически помещать в хранилище объекты, унаследованные от TableEntity – при этом сам SDK позаботится о том, как они будут преобразованы к атрибутам. Запрашивать данные из хранилища мы сможем с помощью простых и привычных LINQ-запросов, получая в результате коллекции объектов.

Для начала опишем класс для хранения одного измерения:

public enum WeatherInfoSource { WeatherService, Device };
public enum ReadingType { Temperature, Humidity, Pressure, Luminocity };

public class WeatherRecord : TableEntity
{

    public WeatherRecord(DateTime When, double Reading, WeatherInfoSource src, ReadingType type)
    {
        this.Reading = Reading;
        this.WeatherInfoSource = src;
        this.When = When;
        this.ReadingType = type;
        PartitionKey = string.Format("{0}.{1}", When.Year, When.Month);
        RowKey = string.Format("{0}-{1}:{2}.{3}.{4},{5}:{6}", src, type, When.Year, When.Month, When.Day, When.Hour, When.Minute);
    }

    public WeatherRecord() { }

    public DateTime When { get; set; }

    public int iWeatherInfoSource
    {
        get; set;
    }
    public WeatherInfoSource WeatherInfoSource
    {
        get { return (WeatherInfoSource)iWeatherInfoSource; }
        set { iWeatherInfoSource = (int)value; }
    }

    public int iReadingType { get; set; }

    public ReadingType ReadingType
    {
        get { return (ReadingType)iReadingType; }
        set { iReadingType = (int)value; }
    }
    public double Reading { get; set; } 

}

Основные поля в этом классе:

  • WeatherInfoSource – показывает, взято ли измерение из погодного сервиса или из погодной станции
  • ReadingType – описывает тип измерения: температура, давление, влажность, освещенность
  • Reading – само значение измерения
  • When – время проведения измерения

В этом классе есть две важные тонкости:

  • Мы не можем хранить в TableEntity данные не-примитивных типов. Поэтому вместо публично доступных свойств WeatherInfoSource и ReadingType перечислимого типа, мы описываем поля iReadingType и iWeatherInfoSource типа int, а WeatherInfoSource/ReadingType делаем свойствами, которые скрывают в себе операции преобразования типов
  • Поля PartitionKey и RowKey определяют, как данные будут распределяться по серверам в случае значительных объемов. Все объекты с одним PartitionKey хранятся вместе. В нашем случае мы будем хранить вместе все данные за одни месяц, т.к. нам их в дальнейшем может понадобится агрегировать. Правильный выбор PartitionKey/RowKey определяет архитектуру распределенного хранения данных, и может сильно влиять на производительность.

Для доступа к данным создадим статический класс WeatherDB, в котором будут находится необходимые функции. В конструкторе класса мы будем инициализировать внутренние поля для доступа к таблице и создавать таблицу, если её не существует:

static CloudStorageAccount CloudStore;
static CloudTableClient CloudTable;
public static CloudTable Table;

static WeatherDB()
{
    string str = "DefaultEndpointsProtocol=http;AccountName=weathermon;AccountKey=...";
    CloudStore = CloudStorageAccount.Parse(str);
    CloudTable = CloudStore.CreateCloudTableClient();
    Table = CloudTable.GetTableReference("WeatherRecord");
    Table.CreateIfNotExists();
}

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

Для помещения объекта в таблицу используем следующий метод:

public static async void Record(ReadingType t, WeatherInfoSource src, double x)
{
    var rd = new WeatherRecord(CurrentDateTime, x, src, t);
    await Table.ExecuteAsync(TableOperation.Insert(rd));
}

Механика очень проста – мы создаем новый объект, после чего вызываем операцию вставки этого объекта в хранилище. Что интересно – операция асинхронная, поскольку при этом инициируется подключение к облачному сервису через интернет, что может занять некоторое время. Аналогичным образом асинхронно устроена и операция получения всех данных из хранилища:

public static IEnumerable<WeatherRecord> GetData()
{
    var q = new TableQuery<WeatherRecord>();
    return WeatherDB.Table.ExecuteQuery(q);
}

Более интересная операция – это извлечение последних заданных измерений из таблицы. Это нужно, чтобы в приложении в дальнейшем показывать два значения температуры, полученных с сенсора и из погодного сервиса. Следующая функция возвращает массив последних значений заданного ReadingType, по одному измерению для каждого WeatherInfoSource:

public static WeatherRecord[] GetCurrentReading(ReadingType type)
{
    var data = (from z in WeatherDB.Table.CreateQuery<WeatherRecord>()
               where z.When > DateTime.Now.AddMinutes(-10)
               // where z.ReadingType == type
               select z).ToList();
    var res = from z in data
              where z.ReadingType == type
              group z by z.WeatherInfoSource into g
              select g.OrderByDescending(x => x.When).First();
    return res.ToArray();
}

Этот метод устроен достаточно хитро. Проблема в том, что мы не можем использовать функции сортировки данных на сервере, поскольку данные возможно распределены по серверам. В первом LINQ-запросе, который выполняется на сервере, нам доступны только очень простые операции. Поэтому вначале мы выбираем из таблицы в память все данные за последние 10 минут – чтобы уменьшить размер обрабатываемых данных. При этом мы даже не может отфильтровать их по ReadingType – соответствующая строчка закомментарена.

Далее мы производим запрос в память – и здесь нам уже доступна вся мощь LINQ, включая group into, с помощью которого мы и выделяем требуемые нам данные.

Worker Role для доступа к погодному сервису

Теперь запрограммируем Worker Role, который будет обращаться к погодному сервису и раз в 10 минут считывать значение погодных параметров. Для этого нам нужно будет зарегистрироваться в сервисе OpenWeatherMap и получить оттуда API Key для нашего приложения. Для доступа к OpenWeatherMap из нашего сервиса будем использовать библиотеку OpenWeatherMapClient.

Worker Role – это процесс, который запускается в облаке и всё время работает. Основной его функционал описывается одним классом, а главная функция – это асинхронная функция RunAsync:

private async Task RunAsync(CancellationToken cancellationToken)
{
    while (!cancellationToken.IsCancellationRequested)
    {
        var Weather = await WeatherClient.CurrentWeather.GetByName(TargetCity,MetricSystem.Metric);
        WeatherDB.Record(ReadingType.Temperature, WeatherInfoSource.WeatherService, Weather.Temperature.Value);
        WeatherDB.Record(ReadingType.Pressure, WeatherInfoSource.WeatherService, Weather.Pressure.Value);
        WeatherDB.Record(ReadingType.Humidity, WeatherInfoSource.WeatherService, Weather.Humidity.Value);
        await Task.Delay(1000*60*NumMinutes);
    }
}

В ней всё очень просто: считываем текущую погоду через OpenWeatherMapClient, а потом вызываем описанные ранее функции для записи данных в хранилище. Обратите внимание, что поскольку функция асинхронная – она может спокойно “засыпать” на длительное время с помощью Task.Delay, освобождая процессорное время для других задач. Кроме того, в асинхронной функции можно удобно вызывать другие асинхронные методы – как это мы делаем с вызовом API OpenWeatherMap. Пока производится удалённый вызов, наш сервис также находится в состоянии спокойного ожидания.

Нам осталось в методе OnStart() инициализировать все необходимые переменные, в т.ч. параметры: название города и периодичность выполнения в минутах:

TargetCity = CloudConfigurationManager.GetSetting("City");
WeatherClient = new OpenWeatherMapClient("...OpenWeatherMapAPIKey...");
NumMinutes = int.Parse(CloudConfigurationManager.GetSetting("NumMinutes"));

Чтобы мы могли использовать CloudConfigurationManager.GetSetting, нам надо прописать соответствующие параметры в конфигурации нашего облачного сервиса. Идем в основной раздел решения в Visual Studio, отвечающий за облачный сервис (WeatherMonitor), выбираем WeatherMonitorWorker, и в разделе “параметры” добавляем два параметра NumMinutes и City типа string.

image

Всегда когда возможно стоит использовать такой способ конфигурации сервисов, поскольку соответствующие значения можно будет менять как из интерфейса Visual Studio, так и из портала Windows Azure. Это очень удобно!

Теперь уже можно запустить наше приложение и проверить его в работе! Убедится, что данные записываются в хранилище, можно с использованием очень удобного инструмента Azure Storage Explorer.

Отображение данных на веб-портале

У нашего приложения также есть веб-составляющая WeatherMonitorWeb. Это приложение ASP.NET Web Forms, в которое на главную страницу мы добавим элемент управления Chart Control для отображения графика измерений, и пару элементов управления для выбора параметров:

    <asp:Chart ID="MainChart" runat="server" Height="600" Width="800">
        <series>
            <asp:Series Name="MainSeries" ChartArea="MainChartArea" ChartType="Line" Color="Black"/>
            <asp:Series Name="RealSeries" ChartArea="MainChartArea" ChartType="Point" Color="Red" />
        </series>
        <chartareas>
            <asp:ChartArea Name="MainChartArea" />
        </chartareas>
    </asp:Chart>
<br /><br />
<asp:DropDownList runat="server" AutoPostBack="True" ID="ddDispType">
    <asp:ListItem Value="Temperature" />
    <asp:ListItem Value="Pressure" />
    <asp:ListItem Value="Humidity" />
    <asp:ListItem Value="Luminocity" />
</asp:DropDownList>
<asp:DropDownList runat="server" AutoPostBack="True" ID="ddTimePeriod">
    <asp:ListItem Value="Day" />
    <asp:ListItem Value="Week" />
    <asp:ListItem Value="Month" />
    <asp:ListItem Value="Year" />
</asp:DropDownList>

Здесь в Chart Control мы накладываем друг на друга два графика: реальные результаты измерений (которые мы научимся измерять чуть позже), и сохраненные нами значения из погодного сервиса.

Наполнять график данными мы будем в методе Page_Load() нашей странички. Для начала, определим по положению элементов управления срок измерений, который нам надо отображать, и тип измерений:

ReadingType t = ReadingType.Temperature;
DateTime fr = DateTime.Now.AddYears(-1);
switch (ddTimePeriod.SelectedValue)
{
    case "Day":
        fr = DateTime.Now.AddDays(-1); break;
    case "Month":
        fr = DateTime.Now.AddMonths(-1); break;
    case "Year":
        fr = DateTime.Now.AddYears(-1); break;
    case "Week":
        fr = DateTime.Now.AddDays(-7); break;
}
switch(ddDispType.SelectedValue)
{
    case "Temperature":
        t = ReadingType.Temperature; break;
    case "Pressure":
        t = ReadingType.Pressure; break;
    case "Humidity":
        t = ReadingType.Humidity; break;
    case "Luminocity":
        t = ReadingType.Luminocity; break;
}

Далее собственно переходим к получению данных и их отображению с помощью DataBinding:

var db = WeatherDB.GetData();
var data = from z in db
            where z.WeatherInfoSource == WeatherInfoSource.WeatherService
            where z.ReadingType == t
            where z.When > fr
            orderby z.When descending
            select new { When = z.When, Data = z.Reading };
MainChart.Series[0].Points.DataBind(data,"When", "Data", "");
var rdata = from z in db
               where z.WeatherInfoSource == WeatherInfoSource.Device
               where z.ReadingType == t
               where z.When > fr
               orderby z.When descending
               select new { When = z.When, Data = z.Reading };
MainChart.Series[1].Points.DataBind(rdata, "When", "Data", "");

Здесь мы используем не очень хороший подход – сначала запрашиваем все данные в память с помощью описанного ранее метода GetData(), а уже потом осуществляем их выборку и сортировку. Оставляю читателю оптимизацию этого кода в качестве самостоятельного упражнения (Pull Requests приветствуются) – это можно сделать, ограничив объем передаваемых данных нужным периодом, как это сделано в функции GetCurrentReading выше.

На этом этапе вы должны быть в состоянии запустить сервис на своём компьютере, дать ему поработать некоторое время, и увидеть график температуры, собранной с OpenWeatherMap. Теперь самое время разместить проект в реальном облаке – для этого выберем облачный сервис WeatherMonitor правой кнопкой и используем “Опубликовать”.

При этом скорее всего у вас возникнут проблемы, связанные с особенностью реализации Chart Control. Дело в том, что этот элемент использует временные файлы в системной директории, которая при облачном развертывании оказывается защищена от записи. Для борьбы с этой проблемой, надо добавить в Web.Config следующие строчки конфигурации:

<configuration>
  <appSettings>
    <add key="ChartImageHandler" value="storage=session;timeout=20;" />

Создаем сервис WebAPI

Для того, чтобы мы могли в дальнейшем записывать данные от контроллера NetDuino, нам необходимо предусмотреть простое API, чтобы запрос по некоторому адресу URL автоматически приводил к записи соответствующего значения в таблицу. Для этого проще всего использовать технологию Web API, которую можно развернуть вместе с сайтом на базе ASP.NET. Подробнее этот процесс описан в этой англоязычной статье.

Первым делом, в файл Global.asax.cs необходимо добавить следующий код настройки маршрутизации, выполняемый при запуске приложения:

void Application_Start(object sender, EventArgs e)
{
    RouteTable.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = System.Web.Http.RouteParameter.Optional });
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
}

Теперь для того чтобы создать API по записи температуры, нам надо создать класс контроллера TemperatureController.cs – проще всего это сделать через добавление в проект класса Web API:

image 

Контроллер позволяет определять методы, которые будут вызываться при получении GET или POST-запросов по адресам типа /api/temperature или /api/temperature/13. Например, для записи температуры в базу по GET-запросу, переопределим следующий метод:

// GET api/<controller>/5
public async Task<string> Get(int id)
{
    await WeatherDB.RecordSuccess(ReadingType.Temperature, WeatherInfoSource.Device, id / 100.0);
    return DateTime.Now.ToString();
}

Все преобразование входных и выходных параметров будут проведены автоматически инфраструктурой Web API. В нашем случае мы принимаем на вход температуру (умноженную на 100, чтобы использовать целый тип), а возвращаем текущую дату и время – это будет полезно, если мы захотим использовать контроллер Arduino без часов реального времени.

Аналогичным образом мы опишем метод, который по обычному запросу по адресу /api/temperature без параметров вернет последние измерения температуры. Все измерения возвращаются в формате JSON:

public WeatherRecord[] Get()
{
    return WeatherDB.GetCurrentReading(ReadingType.Temperature);
}

Аналогично создаются контроллеры для давления, влажности, освещенности.

Теперь мы можем, сделав запрос по адресу http://weathermon.cloudapp.net/temperature/-13, записать в таблицу любое измерение. Это измерение можно будет сразу посмотреть на графике.

Проектируем аппаратное устройство

Для реализации устройства, которое будет измерять температуру, давление и освещенность за окном, используем замечательный микроконтроллер NetDuino Plus 2. Его основной плюс состоит в том, что он имеет встроенный контроллер Ethernet, а также является совместимым в Arduino по форм-фактору.

Для измерения давления и температуры будем использовать замечательный датчик BMP-180, а для измерения освещенности – классическую схему с фоторезистором. Принципиальная схема устройства приведена ниже:

Schematic

Board

Для сборки устройства я использовал обычный тонкий провод и паяльник, собрав схему прямо “на проводах” без использование макетной платы (конструкция видна на фото вверху статьи). Провод Ethernet втыкался прямо из марштуризатора (важно, чтобы на маршрутизаторе был включен DHCP), питание – от стандартного блока питания на 9 вольт.

В полученном дизайне значение освещенности мы будем снимать в аналогового входа 0, а сенсором давления/температуры будем управлять через последовательную шину I2C.

Пишем программу для NetDuino

Я был искренне и приятно удивлен, но программа для NetDuino пишется на слегка урезанном .NET – .NET Micro Framework (русскоязычный сайт). В отличие от Arduino, где работа с Ethernet Shield происходит достаточно сложно, здесь нам сразу становятся доступны все функции .NET по работе с сетью.

Для работы с сенсором я использовал библиотеку SensorLib. Поскольку программирование в .NET требует Visual Studio 2013 (вот тут рассказывается про работу .NET MF с VS2013), я создал отдельный проект (код на GitHub).

Вся функциональность программируется в одном главном файле в функции Main. Вначале мы инициализируем порты и создаем объект для работы с сенсором:

AnalogInput photo_in = new AnalogInput(Pins.GPIO_PIN_A0);
OutputPort pin = new OutputPort(Pins.GPIO_PIN_SDA, true);
Thread.Sleep(200);
pin.Write(false); pin.Dispose();

Debug.Print("Initializing sensor library...");
SensorLibrary.BMP180 sensor = new SensorLibrary.BMP180();
sensor.Init(SensorLibrary.BMP180.Mode.BMP085_MODE_STANDARD);

После этого, основная работа контроллера заключена в бесконечный цикл. Он проводит измерение, отправляет его в интернет и затем засыпает на заданное количество минут:

while (true)
{
    double pressure = sensor.GetPressure() / 100.0F;
    double pressuremm = pressure * 0.75006375541921;
    Debug.Print("Pressure:    " + pressuremm.ToString() + " mmHg");
    double temperature = sensor.GetTemperature();
    Debug.Print("Temperature: " + temperature.ToString() + " C");
    int lum = photo_in.Read();
    Debug.Print("Luminocity: " + lum.ToString());
    record("Temperature", (int)(temperature * 100));
    record("Pressure", (int)(pressuremm * 10));
    record("Luminocity", lum);
    Thread.Sleep(1000 * 60 * 10);
}

Для отправки в интернет служит функция record – она инициирует запрос в интернет по соответствующей URL и игнорирует результат:

private static void record(string measurement, int value)
{
    TouchUrl("http://weathermon.cloudapp.net/api/" +
	  measurement + "/" + value.ToString());
}

private static string TouchUrl(string url)
{
    using (var req = (HttpWebRequest)WebRequest.Create(url))
    {
        req.Method = "GET";
        using (var resp = req.GetResponse())
        {
		  // Если нужно - тут можем прочесть результат
        }
    }
}

На всякий случай мы также предусмотрели промежуточную печать с помощью Debug.Print – это очень удобно, поскольку когда плата подключена к компьютеру по USB, мы можем сразу видеть соответствующие строки прямо в Visual Studio. Также .NET MicroFramework поддерживает удобную отладку прямо из Visual Studio.

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

Если вы посмотрите внимательнее на историю данных – в некоторых местах реальные измерения отсутствуют. Это происходит потому, что мы не предусмотрели обработку исключений нашим контроллером. Если вдруг доступ в интернет временно теряется, приложение генерирут исключение, и бесконечный цикл останавливается. Для более устойчивой работы контроллера требуется более аккуратно подходить к вопросам исключительных ситуаций (Pull Request Welcome)!

Реализуем приложение для Windows Phone

Заключительным аккордом нашего упражнения станет реализация приложения для Windows Phone, которое позволит отображать текущую температуру. Будем использовать созданное нами WebAPI, которое возвращает необходимые данные в формате JSON. Для работы с такими данными нам потребуется подключить к вновь созданному проекту Windows Phone библиотеку JSON.NET (через NuGet – install-package Newtonsoft.Json).

Чтобы десериализовывать данные, нам потребуется класс, аналогичный созданному ранее WeatherRecord. Однако подключить WeatherLib напрямую к нашему проекту мы не можем, поскольку она не поддерживает платформу Windows Runtime, да и тащить за собой в приложения для смартфона всю облачную функциональность не очень правильно. Поэтому мы можем воспользоваться Cut-And-Paste, либо создать упрощенный вариант класса WeatherRecord SimpleWeatherRecord.

Для получения результата вызова JSON-метода создадим функцию GetReading, которая будет вызывать нужные метод API и возвращать массив последних измерений типа SimpleWeatherRecord:

public async Task<SimpleWeatherRecord[]> GetReading(string rtype)
{
    var cli = new HttpClient();
    var str = string.Format("http://weathermon.cloudapp.net/api/{0}", rtype);
    var s = await cli.GetStringAsync(new Uri(str));
    return Newtonsoft.Json.JsonConvert.DeserializeObject<SimpleWeatherRecord[]>(s);
}

Обратите внимание на естественную асинхронность метода, и на простоту преобразования из формата JSON в строго типизированный массив объектов – одной строчкой!

Далее, создадим на главной странице набор элементов управления для отображения результатов. Я не буду вдаваться в подробности XAML-дизайна (исходный код можно посмотреть тут), приведу лишь пример получения температуры (остальные значения получаются аналогично, полный текст файла с кодом тут):

private async void Page_Loaded(object sender, RoutedEventArgs e)
{
    var ind = StatusBar.GetForCurrentView().ProgressIndicator;
    await ind.ShowAsync();
    ind.Text = "Getting temperature...";
    TempService.Text = "--"; TempOut.Text = "--";
    var temp = await GetReading("temperature");
    foreach(var x in temp)
    {
        if (x.iWeatherInfoSource == 0) // данные из сервиса
            TempService.Text = x.Reading.ToString();
        else // данные с термометра
            TempOut.Text = x.Reading.ToString();
    }
    await ind.HideAsync();
}

Полученные с сервиса данные обычно содержат массив из двух измерений – последнее измерение температуры на улице и из OpenWeatherMap. Пройдясь по массиву циклом, мы помещаем каждое из измерений в соответствующий ему TextBlock. Если вдруг за последние 10 минут не было сделано измерения (это возможно, если например наше устройство NetDuino перестало передавать данные в облако), то соответствующей записи не будет в массиве, и текстовое поле будет отображать значение “—“.

Конечно, было бы правильнее сразу сделать это приложение универсальным приложением Windows. Но я оставлю это в качеству упражнения читателям, и буду ждать ваших Pull Requests.

Заключение

Статья получилась достаточно длинной, но, надеюсь, полезной! Она показывает, как на одном языке программирования C# и в одной среде (Visual Studio) вы можете создавать совершенно разное программное обеспечение – облачные сервисы, приложения для смартфонов, программы для микроконтроллеров. Программист все время использует (почти) одну и ту же библиотеку базовых функций, удобную среду программирования и отладки. Всё это делает платформу Майкрософт приятной в использовании, настолько, что я решил потратить часть новогодних каникул на проектирование этой погодной станции.

В статье содержится ряд пожеланий по улучшению. Мне будет очень любопытно, если кто-то подключится и в режиме pull request улучшит или дополнит мои примеры. Давайте вместе улучшать примеры в лучших традициях Open Source!

Эта статья – по всей видимости не последняя про погодную станцию, я расскажу вам про обработку получаемых данных на F#, а также возможно про создание аналогичной погодной станции на Arduino, как на более распространенном контроллере. Если у вас есть пожелания или идеи, о чем лучше рассказать – пишите мне! В любом случае, мне будет очень интересно узнать (в комментариях или вконтакте), сколько человек дочитало статью до конца, что показалось полезным, а что – нет.

Счастливого кодирования!

2 comments