Stall Status: терпи, все кабинки заняты
Stall Status — это сделанное в Silverlight мини-приложение для боковой панели Vista, в котором применяется протокол беспроводной связи Z-Wave и дверные датчики для уведомления о том, свободны или заняты кабинки в туалетной комнате офиса.
Джерри Браннинг (Jerry Brunning)
Сложность: низкая
Необходимое время: 1-3 часа
Затраты : $50-$100
ПО : Visual C# Express Edition
Оборудование : ControlThink Z-Wave SDK (EN), дверной датчик Hawking Technologies Z-Wave HRDS1 (EN)
Загрузки: загрузить (EN)
Stall Status
Всем нам знакомо чувство рождения новой гениальной идеи, которую обязательно надо реализовать. Когда Джон Раушенбергер (Jon Rauschenberger), технический директор компании Clarity, появился в моем кабинете с «блестящей идеей» системы слежения за состоянием «занято/свободно» кабинок туалетной комнаты, я сразу понял: он сейчас именно в таком состоянии. Как раз на той неделе я исследовал протокол беспроводной связи Z-Wave для использования его в одном проекте и решил, что «Stall Status» (состояние кабинки) — неплохая возможность получше разобраться с Z-Wave.
В этой статье я покажу вам, как писать управляемый код для взаимодействия с устройствами Z-Wave. Я также продемонстрирую применение политик доступа Silverlight 2.0 для создания «подключенных клиентов»: клиентских приложений, установивших постоянное TCP-соединение с удаленным сервером, которым вследствие этого не надо опрашивать сервер для выявления изменения состояния. И наконец, я опишу всю последовательность развертывания приложения Silverlight 2.0 в качестве мини-приложения боковой панели Windows Vista.
Что такое Z-Wave?
Z-Wave — это протокол беспроводной связи в радиодиапазоне для работы в системах, не требующих сигнала большой мощности и высокой пропускной способности. Типичная сфера применения устройств Z-Wave — приложения мониторинга и управления, такие как домашняя автоматизация. Применяемые устройства — датчики, переключатели и двигатели. Устройства Z-Wave совместимы между собой — устройства, маркированные как поддерживающие протокол Z-Wave, должны обеспечивать совместную работу.
Сеть Z-Wave состоит из одного или нескольких устройств и центрального контроллера. Контроллерами бывают автономные приборы (зачастую портативные) и программно-управляемые комплекты. Маршрутизация сообщений в сетях Z-Wave производится автоматически, предварительной настройки маршрутов для устройств не требуется. То есть, если вам нужна связь между несколькими устройствами, например дверными датчиками и сигнализацией, эти устройства могут передавать сообщения между собой, обеспечивая доставку сообщения требуемому адресату. Такое взаимодействие расширяет возможности сети по сравнению с подходом, когда устройства требуют непосредственного подключения между собой. Например, если у вас есть три устройства A, B и C устройство A может взаимодействовать с C, даже если C находится вне досягаемости устройства A: это делается автоматически через устройство B.
Моя простая сеть Z-Wave для Stall Status состоит из дверных датчиков, сообщающих об открывании и закрывании дверей, а также программируемый контроллер. Для двери каждой кабинки я использовал дверные датчики с батареечным питанием HRDS1 от Hawking Technologies. HRDS1 относится к устройствам, «включаемым по случаю», — оно находится в спящем режиме, пока не обнаружит, что дверь открылась или закрылась. Устройства такого типа применяются для установки на окна и двери в системах сигнализации. В качестве Z-Wave-контроллера я использовал отличное программно-управляемое устройство и SDK от ControlThink (EN) за $70. Контроллер представляет собой USB-адаптер и набор API для связи и взаимодействия с устройствами. ControlThink включает несколько демонстрационных .Net-приложений, но они не поддерживают работу с какими-то конкретными устройствами и приспособить их к нужным устройствам бывает сложно.
Использование же ControlThink SDK проблем не вызывает. Подключите USB-адаптер, установите ссылку на его DLL в своем .Net-приложении и одной строкой кода подключитесь к контроллеру. После подключения вы можете управлять своей сетью, добавляя, удаляя, просматривая устройства и выполняя другие действия.
В завершение я написал три программы для реализации Stall Status:
- Административную утилиту для добавления, удаления и просмотра устройств Z-Wave в моей сети. Она называется StallStatusAdmin.
- Службу Windows для отслеживания поступающих от дверных датчиков событий открывания/закрывания и уведомления о них любых подключенных клиентов. Она называется StallStatusController.
- Клиентское приложение Silverlight 2.0 (интерфейс конечного пользователя), которое подключается к моей Windows-службе для получения уведомлений. Оно называется StallStatusSilverlight.
Настройка сети Z-Wave
Административное приложение — это обычный EXE-файл Windows Forms. Оно позволяет мне управлять сетью: добавлять, удалять и просматривать устройства. Благодаря ControlThink SDK реализовать его было просто, в большинстве случаев требовалась всего лишь пара строк кода. У каждого устройства Z-Wave есть некоторый механизм, позволяющий устанавливать устройство в «программируемый режим». В HRDS1 это кнопка с задней стороны, при нажатии на которую устройство переводится в программируемый режим и может принимать программный код с контроллера.
В листинге 1 продемонстрировано добавление устройства к сети. Добавление устройства включает регистрацию устройства на контроллере и создание связи между контроллером и объединяющей группой данного устройства для передачи с него сообщений на контроллер. После регистрации каждого устройства ему присваивается уникальный идентификатор NodeID, позволяющий отличить три датчика друг от друга.
1: ZWaveController controller = Connect();
2: MessageBox.Show("After pressing OK, press the \"programming\" button on your device.");
3: ZWaveDevice device = controller.AddDevice();
4:
5: device.Groups[1].Clear();
6: device.Groups[1].Add(controller.Devices.GetByNodeID(controller.NodeID));
7: VB.Net
8: Dim controller As ZWaveController = Connect()
9: MessageBox.Show("After pressing OK, press the ""programming"" button on your device.")
10: Dim device As ZWaveDevice = controller.AddDevice()
11:
12: device.Groups(1).Clear()
13: device.Groups(1).Add(controller.Devices.GetByNodeID(controller.NodeID))
Листинг 1. Добавление устройства в сеть
Получение списка зарегистрированных устройств сводится к перебору коллекции Devices на контроллере.
1: string result = "";
2: ZWaveController controller = Connect();
3:
4: if (controller.Devices.Count > 0) {
5: foreach (ZWaveDevice d in controller.Devices) {
6: result += d.GetType().ToString() + " as Node #" + d.NodeID + " PollEnabled = " + d.PollEnabled + "\r\n";
7: }
8: }
9: else {
10: result = "No devices found on the network";
11: }
12: MessageBox.Show(result);
Листинг 2. Получение списка устройств в сети
Обработка событий Z-Wave
Зарегистрированные в сети устройства отправляют контроллеру уведомления о событиях при каждом срабатывании соответствующих датчиков. Следующий наш шаг — написать код для приема уведомлений о событиях и передачи их всем подключенным клиентским приложениям. Для реализации этого механизма мы напишем службу Windows, отслеживающую события датчиков.
Все, что нам надо для отслеживания событий устройств Z-Wave, нам предоставляет ControlThink SDK. Мы просто подключаемся к контроллеру и отслеживаем событие LevelChanged. Делается это в методе Run, вызываемом из функции OnStart нашей службы.
1: public void Run() {
2: try {
3: _controller = new ZWaveController();
4: try {
5: _controller.Connect();
6: }
7: catch (Exception ex) {
8: throw new Exception("Unable to connect.\r\n" + ex.ToString());
9: }
10:
11: _controller.LevelChanged += new ZWaveController.LevelChangedEventHandler(_controller_LevelChanged);
12:
13: _policyListener = new PolicyListener();
14: _listener = new SocketListener();
15: _listener.Port = 4530;
16:
17: new System.Threading.Thread(new System.Threading.ThreadStart(_listener.StartSocketServer)).Start();
18: new System.Threading.Thread(new System.Threading.ThreadStart(_policyListener.StartSocketServer)).Start();
19: new System.Threading.Thread(new System.Threading.ThreadStart(ClearRecent)).Start();
20: }
21: catch (Exception ex) {
22: Logger.WriteException(ex);
23: }
24: }
Листинг 3. Отслеживание событий Z- Wave после запуска службы
Кроме отслеживания событий от контроллера ZWave, в нашем методе Run также устанавливаются использующие сокеты слушатели для взаимодействия с клиентским приложением Silverlight, о котором мы поговорим позже. Важный момент, который надо отметить, — это применение потоков в методе Run. Напомню, что он вызывается непосредственно из кода OnStart нашей службы. Если вы заблокируете код OnStart, вы увидите в Диспетчере служб, что эта служба зависла с сообщением «Запускается ...». Все, что может привести к блокировке или длительному выполнению (в том числе, слушатель сокета), должно работать в отдельном потоке.
Когда контроллер обнаруживает изменение состояния любого датчика, он генерирует событие LevelChanged для нашей службы. Она обрабатывает событие LevelChanged в методе _controller_LevelChanged. Сигнатура события LevelChanged выглядит так:
void _controller_LevelChanged(object sender, ZWaveController.LevelChangedEventArgs e)
LevelChangeEventArgs содержит сведения о конкретном устройстве, инициировавшем данное событие. В нашей сети три дверных датчика. При срабатывании любого из них генерируется событие LevelChanged, но мы можем определить уникальный идентификатор сработавшего датчика NodeID, проанализировав LevelChangedEventArgs. Вот и все, что касается взаимодействия в сети Z-Wave.
Silverlight 2.0
Чтобы начать реальное применение Stall Status, надо после создания серверной архитектуры построить клиентское приложение. Я создал клиентское приложение в Silverlight в качестве упражнения по использованию политики доступа Silverlight 2.0 для реализации междоменных сетевых вызовов из Silverlight-клиента. Кроме того, я знал, что приложения Silverlight легко устанавливать в качестве мини-приложений панели Windows Vista, и именно такую программу я хотел создать.
Рис. 1. Интерфейс пользователя Stall Status
Основной пользовательский интерфейс программы весьма прост — это три кружка, каждый из которых соответствует кабинке. Кружок имеет зеленый цвет, если кабинка свободна, красный — если занята. Наиболее сложным моментом при разработке клиентского приложения была реализация подключения к службе и отслеживание изменений состояния. Мы создаем постоянное TCP-соединение и ожидаем поступления данных от службы через сокет. Учитывая особенности работы дверного датчика Z-Wave, такой подход гораздо эффективней метода опроса. Датчики большую часть времени находятся в спящем режиме, что позволяет тратить энергию только во время передачи сигнала при открывании или закрывании двери. Если мы будем постоянно опрашивать эти устройства, то быстро угробим их батарейки. Большую часть времени изменений в состоянии двери не происходит, значит, опрос будет бессмысленным.
В Silverlight есть некоторые механизмы безопасности, связанные с сетевым взаимодействием, в частности, с сокетами TCP. Причиной является тот факт, что Silverlight-приложения исполняются в контексте пользовательского обозревателя, поэтому требуется особый контроль, ограничивающий типы взаимодействий, которые могут инициировать Silverlight-приложения. Для установления сокет-соединений, Silverlight 2.0 требует наличия на сервере, с которым осуществляется взаимодействие, политики доступа. Эта политика, по сути, является механизмом запроса и подтверждения, предохраняющим Silverlight-приложение от подключения к случайному серверу, который может маскироваться под конечного пользователя.
Политика доступа
Чтобы клиент, реализованный в Silverlight 2.0, мог подключиться к серверу с помощью сокетов, на этом сервере должна быть реализована политика доступа (Access Policy). Проверку наличия политики доступа выполняет инфраструктура Silverlight, клиентским приложениям в этом плане ничего делать не надо. Между тем, если на сервере не реализована необходимая политика, Silverlight сгенерирует исключение «Отказано в доступе», которое клиентское приложение должно быть готово обработать. Все это очень напоминает схему сетевого взаимодействия Flash-приложений. На самом деле, Silverlight может напрямую использовать файлы политик Flash, наряду с файлами политик собственно Silverlight. В приложении Stall Status мы собираемся применять политики Silverlight.
Когда Silverlight-приложение пытается открыть сокет для связи с сервером, Silverlight автоматически ищет политику доступа на этом сервере, установив TCP-соединение через порт 943, через который отправляется запрос политики. Запрос политики — это просто XML-строка: <policy-file-request/>. Ответственность за получение этого запроса и корректный ответ на него лежит на разработчике серверной службы; если он не будет обработан, Silverlight сгенерирует исключение на клиенте. Все это подробно описано в документации MSDN (EN), в разделе, посвященном новым способам обеспечения безопасности в Silverlight 2.0.
Чтобы наш сервер отвечал на запросы политики доступа, нам надо реализовать сетевой слушатель для порта 943, который будет отвечать на запросы политики доступа, поступающие от Silverlight. Дэн Уолин (Dan Wahlin, EN) опубликовал в своем блоге состоящую из нескольких частей статью как раз на эту тему. Используемый в Stall Status слушатель и много другого кода, связанного с сокетами, я скопировал из его примера. Рекомендую прочитать его публикацию, это отличный пример создания клиентов с TCP-сокетами в Silverlight 2.0.
Наш класс-слушатель выполняется в Windows-службе и просто устанавливает слушатель на порт 943. При получении запроса политики он возвращает следующий ответ:
1: <?xml version="1.0" encoding ="utf-8"?>
2: <access-policy>
3: <cross-domain-access>
4: <policy>
5: <allow-from>
6: <domain uri="*" />
7: </allow-from>
8: <grant-to>
9: <socket-resource port="4530" protocol="tcp" />
10: </grant-to>
11: </policy>
12: </cross-domain-access>
13: </access-policy>
Этот ответ позволяет Silverlight открыть порт 4530, на котором наша служба обрабатывает запросы от Silverlight-клиентов.
Почему это важно? Дело в том, что без реализации политики доступа ничто не мешает вредоносным Silverlight-приложениям соединиться с сервером без должных разрешений. Например:
- Предположим, у вас установлено соединение с вашим банком. Вредоносное Silverlight-приложение с сайта A может попытаться связаться с вашим банком, используя существующие регистрационные данные, которые могут храниться как файлы cookie. При использовании политики доступа ваш банк должен иметь подтверждение политики, что Silverlight-приложения с сайта А могут с ним соединяться.
- Допустим, вы зашли на сайт А и загрузили вредоносное Silverlight-приложение. Это приложение потенциально может войти в вашу корпоративную интрасеть, поскольку олицетворяет вас. При наличии политики доступа это будет невозможно.
Постоянные соединения клиентов
После реализации политики доступа можно приступать к созданию Silverlight-клиента, ожидающего сообщений об изменениях состояния с сервера. Silverlight 2.0 позволяет устанавливать сокет-соединения только через порты 4502-4534. Наша служба Windows использует порт 4530. Она создает слушатель на этом порту и ждет сообщений от Silverlight-клиента.
1: _listener = new TcpListener(IPAddress.Any, Port);
2: _listener.Start();
3: while (true)
4: {
5: _waitEvent.Reset();
6: _listener.BeginAcceptTcpClient(new AsyncCallback(OnBeginAccept), null);
7: _waitEvent.WaitOne();
8: }
9:
Листинг 4. Основной слушатель клиентских соединений
Поскольку мы используем асинхронные сокеты, необходимо установить делегат, OnBeginAccept, для работы с соединениями. Ниже приведен код работы с соединениями.
1: _waitEvent.Set();
2: TcpListener listener = _listener;
3: TcpClient client = listener.EndAcceptTcpClient(ar);
4:
5: if (client.Connected)
6: {
7: Logger.WriteLine("Accepted a connection from " + client.Client.RemoteEndPoint.ToString());
8:
9: StreamWriter writer = new StreamWriter(client.GetStream());
10: writer.AutoFlush = true;
11: _clients.Add(writer);
12: writer.WriteLine("Connected.");
13:
14: if (ClientConnected != null)
15: {
16: ClientConnected(this, null);
17: }
18: }
Листинг 5. Прием нового соединения с клиентом
Здесь мы просто добавляем StreamWriter для каждого подключенного клиента в список. Затем, когда мы получим сообщение о событии от дверного датчика, мы сможем просмотреть в цикле этот список и уведомить всех клиентов. Уведомление реализуется путем записи содержащей метки строки в StreamWriter. Также у нас есть код для удаления из списка клиентов, которые не могут выводить данные в свой поток. Таким способом мы обнаруживаем ситуации отключения того или иного клиента.
1: for (int i=_clients.Count-1;i>=0;i--)
2: {
3: try
4: {
5: _clients[i].WriteLine(eventText);
6: }
7: catch (Exception ex)
8: {
9: Logger.WriteException(ex);
10: Logger.WriteLine("Removing client # " + i + " because we failed to send them data.");
11: _clients.Remove(_clients[i]);
12: }
13: }
Листинг 6. Отправка данных об изменении всем клиентам
Передаваемый каждому клиенту текст (в коде он представлен переменной eventText) — это идентификатор NodeID датчика, вызвавшего событие, и состояние двери, соответствующей этому датчику (открыта/закрыта). Silverlight-клиент получает этот текст, разбирает его и изменяет цвет соответствующего кружка.
1: private void UpdateText(string text)
2: {
3: string[] parts = text.Split(' ');
4:
5: if (parts.Length != 2) {
6: return;
7: }
8:
9: int stallNumber = Convert.ToInt32(parts[0]);
10: string status = parts[1].ToLower();
11:
12: StatusIndicator thisStall = null;
13: switch (stallNumber) {
14: case 1:
15: thisStall = this.Stall1;
16: break;
17: case 2:
18: thisStall = this.Stall2;
19: break;
20: default:
21: thisStall = this.Stall3;
22: break;
23: }
24:
25: if (status.Trim() == "open") {
26: thisStall.Status = StallStatus.Open;
27: }
28: else if (status.Trim() == "closed") {
29: thisStall.Status = StallStatus.Closed;
30: }
31: else if (status.Trim() == "recent") {
32: thisStall.Status = StallStatus.Recent;
33: }
34: else {
35: thisStall.Status = StallStatus.Unknown;
36: }
37: }
Осталась лишь одна вешь — реализация концепции «недавно освободившихся» кабинок. Как только какая-то кабинка освобождается, мы считаем ее «недавно освободившейся». В клиентском приложении она отображается желтым кружком. Для этого отслеживается время, прошедшее после открывания двери. В нашей службе есть выделенный поток, обнаруживающий двери, открытые более двух минут назад. Их статус он изменяет с «Недавно освободившихся» на «Открытые».
1: private void ClearRecent()
2: {
3: while (!_wait.WaitOne(10000,false))
4: {
5: var q = from stall in _stalls
6: where stall.Timestamp.AddMinutes(2) < DateTime.Now && stall.State.Equals(DoorState.Recent)
7: select stall;
8:
9: foreach (Stall stall in q)
10: {
11: if (stall.State.Equals(DoorState.Recent)) {
12: stall.State = DoorState.Open;
13: Broadcast(stall.NodeID, DoorState.Open);
14: }
15: }
16: }
17: }
Листинг 8. Сброс флага «Недавно освободившаяся»
Мини-приложение для боковой панели в Windows Vista
Пришло время оформить Silverlight-клиент в виде мини-приложения для боковой панели Vista. Это достаточно просто делается для приложения Silverlight, сложности возникают при работе с 64-разрядными версиями Vista.
Упаковка Silverlight-программы для работы в боковой панели принципиально не отличается от создания версии для локального запуска или работы на веб-странице. Надо просто собрать все файлы приложения, добавить файл манифеста, упаковать их в zip-архив и переименовать этот архив, изменив расширение на .gadget.
Простой способ создания файла манифеста — взять за основу существующий манифест для работающего мини-приложения. Установленные у вас мини-приложения вы можете найти в папке \Пользователи\<имя пользователя>\AppData\Local\Microsoft\Windows Sidebar\Gadgets. Обратите внимание: по умолчанию папка AppData скрыта. Посмотрите папку любого мини-приложения, и вы обнаружите в ней файл gadget.xml. Скопируйте такой файл, откройте его и внесите необходимые изменения. Назначение большинства элементов в этом файле понятно из их названий, однако вы можете посмотреть полное описание в этой статье (EN).
Упаковав свои файлы вместе с манифестом, можете распространять свое приложение. Пользователи смогут запускать файлы .gadget, а Vista автоматически установит ваше приложение в боковую панель.
Рис. 2. Программа Stall Status в качестве мини-приложения Vista
Одно замечание по поводу мини-приложений Silverlight и 64-разрядной версии Windows Vista: поскольку Silverlight является 32-разрядной программой, вы не можете напрямую установить мини-приложение Silverlight в 64-разрядной версии Sidebar.exe (этот компонент работает по умолчанию в 64-битной Vista). В качестве временного решения можете использовать 32-версию Sidebar.exe, хранящуюся в папке \Program Files (x86)\Windows Sidebar\.
Как вы могли убедиться, прочитав эту статью, управление сетью Z-Wave несложно реализовать с помощью .Net, а направлений для творческого применения этой платформы — сколько угодно. Разработка Silverlight-приложений, ориентированных на сеть, более эффективна в версии 2.0 этой технологии. Развертывание построенной в Silverlight программы в качестве мини-приложения боковой панели Vista — совсем несложная задача.