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

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

You are currently browsing the Проекты category.

Рождение новых математиков и программистов

Недавно на кафедре Вычислительной математики и программирования под моим руководством было успешно защищено шесть дипломных проектов с присвоением квалификации “математик-системный программист”. Я хочу коротко рассказать об этих проектах, чтобы остальные студенты примерно понимали, чем я занимаюсь, и с какими темами имеет смысл идти ко мне на диплом. Ну и просто потому, что ребята и девушки проделали отличную работу, которой хочется гордиться!

Diplomaz2015-2

Описываю работы ребят в порядке слева-направо на этой фотографии:

  • Артём Далевич сделал систему Real-Time Bidding на основе явного представления знаний в виде онтологии предметной области, которая была внедрена в реальную мобильную социальную сеть и дала прирост CTR более, чем на 300%, по сравнению с не-контекстной рекламой. Тема работы: “Облачная платформа мобильной рекламы в реальном времени с учетом онтологии интересов пользователя”.
  • Константин Крашенинников провел исследование того, как влияют посты пользователя в твиттере на его решения о покупке в интернет-магазинах (на примере Amazon). Были собраны и проанализированы данные о большом числе реальных аккаунтов, получены интересные выводы и прототип рекомендательной системы товаров на основе анализа социальной сети.
  • Олег Утеков собрал и запрограммировал прототип робота, способного нарисовать портрет пользователя по фотографии. В системе использовался оригинальный механизм рисования на основе схемы с обратной кинематикой, напоминающей руку художника, а также оригинальные алгоритмы построения последовательности штрихов для рисования. В этой работе было и программирование микроконтроллеров, и обработка изображений на базе OpenCV, и облачные технологии Microsoft Azure. Проект уже поддержан по программе BizSpark.
  • Анна Кирилина сделала систему для лексикографов, облегчающую построение словарей. Система способна импортировать текстовые словари (в Rich Text) в единое лингвистическое мета-языковое поле, допускает последующую ручную коррекцию и редактирование, после чего возможен экспорт конкретных словарей для указанных пар языков в разные форматы, от Microsoft Word и HTML с перекрестными ссылками, до мобильного приложения Windows Phone. Работа использовалась для полуавтоматического перевода крымско-татарско-русского словаря в обратный словарь. Тема: “Программная система полуавтоматической генерации словарей для лексикографов”.
  • Сергей Масленников решал смелую задачу создания веб-системы для проведения онлайн-конференций, которая должна сыграть такую же роль в переносе конференций в онлайн-формат, как системы MOOC делают для онлайн-образования. Был проведен анализ информационных процессов, лежащих в основе традиционных научных конференций, и затем выделена минимальная функциональность, не покрываемая традиционными социальными инструментами. Прототип системы использовался для организации импровизированной конференции дипломных работ студентов кафедры. Тема: “Программно-информационное обеспечение научных конференций в сети интернет”.
  • Виктория Королькова защищала систему контроля результатов школьников на уроках физкультуры на основе Kinect, выполненную в рамках совместного проекта с институтом физической культуры, спорта, молодежи и туризма. Система использует Kinect для анализа выполняемых школьниками на уроках упражнений и получения результатов, для чего применяется ряд математических методов, начиная от метода наименьших квадратов и фильтра Калмана, заканчивая статистическими. Система прошла пробное внедрение в нескольких школах Москвы в рамках проекта департамента образования города. Тема: “Система автоматического мониторинга физической подготовки учащихся на основе технологии Microsoft Kinect”.

Надеюсь, некоторые из этих работы будут продолжены (многие были рекомендованы к публикации и внедрению)! К сожалению, в этом году не было работ по распределенному представлению знаний и сетевым функциональным языкам – но в этой области уже есть планы на следующие годы! Если вам интересны исследования в одной из этих областей – пишите, присоединяйтесь!

Ещё раз поздравляю всех ребят с успешной защитой!

Posted Январь 29th, 2015.

Add a comment

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

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

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

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, как на более распространенном контроллере. Если у вас есть пожелания или идеи, о чем лучше рассказать – пишите мне! В любом случае, мне будет очень интересно узнать (в комментариях или вконтакте), сколько человек дочитало статью до конца, что показалось полезным, а что – нет.

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

Posted Январь 26th, 2015.

1 comment

Цифровой терменвокс на базе Leap Motion (powered by .NET)

С момента появления сенсора Kinect for Windows я питал тихую страсть к программированию такого рода устройств – ведь это волшебное чувство, когда написанная тобой программа отзывается на движения человека на расстоянии! Сегодня я расскажу вам про программирование ещё одного похожего устройства – Leap Motion.

6tag_060314-114131

По сути дела, Leap Motion – это небольшой Kinect, который может распознавать положение кистей рук. По замыслу создателей (хорошо отраженных в этом видео), его можно использовать для управления компьютером во множестве различных сценариев. В этом посте мы рассмотрим, как программируется Leap Motion (на платформе .NET), и как можно не его основе создать простой музыкальный инструмент – терменвокс. Вот такой:

Простейший терменвокс на базе Leap Motion

  

Leap Motion для программиста

С точки зрения программиста, Leap Motion предоставляет набор удобных API (документированных тут), которые позволяют:

  • С высокой частотой (более, чем 60-100 кадров в секунду) получать модель пространства, включающую в себя координаты и скорости пальцев рук, нормали ладоней, положения и скорости различных “инструментов” (Leap Motion хорошо распознает карандаши и авторучки)
  • Распознавать основные жесты: окружность, swipe, нажатие
  • Эмулировать touch surface, т.е. касания некоторой виртуальной поверхности перед экраном
  • Распознавать основные движения рук и пальцев (motions), транслируя их в преобразования поворота, перемещения или масштабирования.

В отличие от Kinect, Leap Motion не предоставляет доступа к 3D-данным, полученным с камеры

Программировать Leap Motion можно на различных языках и платформах, в том числе .NET. В этой статье мы рассмотрим использование первой возможности – доступа к модели руки.

Отслеживаем простейшие движения

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

<Window x:Class="LeapVoxSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="768" Width="1024" Background="Black">
    <Canvas>
        <Ellipse x:Name="ptr" Fill="Yellow" Height="30" Width="30"/>
    </Canvas>
</Window>

Для использования Leap Motion необходимо скачать Leap Motion SDK, и подключить к проекту ссылку на LeapCSharp.NET4.0.dll. Однако это еще не всё – Leap SDK требует также библиотек Leap.dll и LeapCSharp.dll, которые должны находится в одной директории с выполняемым файлом проекта. Поэтому найдите эти файлы в директории LeapSDK\lib\x86 (в той директории, где установлен Leap SDK), перетащите их в проект Visual Studio и установите свойства “действия при компиляции = контент, копировать в выходную директорию = копировать, если новее”.

Все общение с контроллером Leap ведется через объект Controller. Создадим такой объект в нашем основном файле MainWindows.xaml.cs:

Controller Leap = new Controller();

Теперь Leap Motion готов предоставлять нам кадры с данными о положении рук. Получать кадры можно двумя способами:

  • Передать в объект Controller экземпляр класса Listener, в котором будет перегружен метод OnFrame. Этот метод будет автоматически вызываться Leap Motion SDK с максимально возможной частотой кадров.
  • Создать свой метод, который будет периодически опрашивать класс Controller и получать с него текущий кадр. Так следует поступать, есть в программе уже есть естественный цикл, или если мы хотим выполнять код в UI-потоке выполнения.

В нашем случае для создания цикла опроса конроллера используем DispatcherTimer, который будет срабатывать 30 раз в секунду:

DispatcherTimer dt = new DispatcherTimer()
    {
        Interval = TimeSpan.FromSeconds(1 / 30)
    };

public MainWindow()
{
    InitializeComponent();
    dt.Tick += dt_Tick;
    dt.Start();
}

В методе dt_Tick будет сосредоточена основная логика отслеживания движений. Мы получаем текущий кадр (Frame), и если в этом кадре видны пальцы – берем первый попавшийся их них и используем его координаты, чтобы изменить положение окружности:

void dt_Tick(object sender, EventArgs e)
{
    var fr = Leap.Frame();
    if (fr!=null && fr.Fingers.Count>0)
    {
        var f = fr.Fingers[0];
        Canvas.SetLeft(ptr, 512 + f.TipPosition.x);
        Canvas.SetTop(ptr, 768-f.TipPosition.y);
    }
}

Мы добились того, что можем перемещать кружок по экрану, передвигая палец над контроллером Leap Motion. Пример кода на данном этапе можно посмотреть на GitHub.

Программная генерация звука различной высоты

Для программной генерации звука различной высоты мы используем библиотеку NAudio. Простейший способ добавить поддержку NAudio в проект – использовать Nuget, набрав в консоли диспетчера пакетов:

install-package NAudio

Процесс генерации звука переменной высоты подробно описан в статье Чарльза Петцольда на MSDN. Если коротко – мы создаем класс, который генерирует синусоидальную волну звука, и передаем её на вход методу NAudio, который эту волну воспроизводит. При этом есть ряд тонкостей: чтобы звук менялся более плавно, необходимо плавно менять частоту звука, и при этом учитывать задержку буферизации аудио. Все эти тонкости учтены в классе PortamentoSineWaveOscillator.cs, который необходимо поместить в проект.

Для генерации звука мы создаем объект типа WaveOut и инициализируем его, передавая экземпляр созданного нами осциллятора:

WaveOut WaveGen = new WaveOut();
PortamentoSineWaveOscillator Osc = new PortamentoSineWaveOscillator(44100,120);

Инициалиацию мы проводим в конструкторе страницы:

WaveGen.Init(Osc);
WaveGen.Play();

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

void dt_Tick(object sender, EventArgs e)
{
    var fr = Leap.Frame();
    if (fr!=null && fr.Fingers.Count>0)
    {
        var f = fr.Fingers[0];
        ...
        // Меняем высоту звука
        var p = Math.Abs(f.TipPosition.y / 2);
        var a = 255 - Math.Abs(f.TipPosition.x);
        if (p >= 0 && p <= 150) Osc.Pitch = p;
        if (a >= 0 && a <= 255) Osc.Amplitude = (short)a;
    }
}

Версию проекта на этой стадии можно получить тут.

Последние штрихи

Чтобы сделать проект чуть более красивым, вместо обычного круга, перемещаемого по экрану, можно добавить спец.эффекты на основе системы частиц (Particle Systems). Я взял за основу вот эту статью на CodeProject, где приводится исходный код WPF-контрола, представляющего собой источник частиц. Заменив им окружность, добавленную на первом шаге, мы получим финальный проект, исходный код которого доступен на GitHub.

Домашнее задание

Если вы захотите продолжить эксперименты, то простор для творчества практически неограничен! В частности, можно:

  • Добавить поддержку многоголосия и управления сразу несколькими пальцами
  • Экспериментировать с другими способами управления высотой звука, например, брать за основу не положение пальца, а его скорость
  • Добавить поддержку жестов для извлечения специальных звуков (сэмплов)

Если вас будет интерес к этой теме (пишите в комментариях), то я продолжу серию статей, добавляя различные дополнительные интересности, связанные с Leap Motion, Kinect и неожиданным использованием мобильных устройств. О чем вам бы хотелось услышать? Например, в этом видео показано, что ещё можно сделать с помощью Leap Motion за пару часов:

Мини-хакатон с Leap Motion – результаты

Posted Март 6th, 2014.

Add a comment