Переключение слайдов презентации с использованием Windows Phone: UDP соединение
В продолжение темы использования Windows Phone в качестве устройства переключения слайдов, представим себе гипотетическую ситуацию, когда сразу нам надо сразу на нескольоких ноутбуках переключать презентацию одновременно. Будем использовать для этого возможности UDP - Mulitcast Group, позволяющий разослать всем присоединившимся к групее сообщение.
Как обычно начнём с серверной части, которая будет слушать сообщения с телефона и выполнять необходимые действия. Можно за основу взять приложение из предыдущей статьи Переключение слайдов презентации с использованием Windows Phone: TCP соединение, здесь же я напишу приложение “заново”.
Итак, мы пишем простое приложение на WinForm, которое будет открывать и запускать презентацию. Кроме этого, оно же будет запускать локальный сервис, который будет слушать по определённому порту данные с телефона и сохранять их во внутреннюю структуру. В рамках решения задачи переключения слайдов, безусловно, гораздо проще просто передавать команды “Вперёд/Назад”, но я хочу показать, как можно работать с “потоком” данных с телефона.
Итак, приступим к созданию WinForm приложения. Это будет простая форма с двумя кнопками,. Одна кнопка для открытия файла презентации, другая для запуска презентации.
Добавим в проект ссылки на InterOp сборки PowerPoint и Office и возьмём код из предыдущего примерра, отвечающий за открытие и запуск презентации:
using System;
using System.Windows.Forms;
using System.IO;
using PowerPoint = Microsoft.Office.Interop.PowerPoint;
namespace PowerPointController
{
public partial class Main : Form
{
private string _pptFileName = "";
PowerPoint.Presentation presesntation;
public Main()
{
InitializeComponent();
}
private void SelectPPT_Click(object sender, EventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
if (ofd.ShowDialog()==DialogResult.OK)
{
_pptFileName = ofd.FileName; }
}
private void RunPPT_Click(object sender, EventArgs e)
{
if (File.Exists(_pptFileName))
{ PowerPoint.Application application = new PowerPoint.Application();
presesntation = application.Presentations.Open(
_pptFileName,
Microsoft.Office.Core.MsoTriState.msoTrue);
PowerPoint.SlideShowSettings sst = presesntation.SlideShowSettings;
sst.ShowType = Microsoft.Office.Interop.PowerPoint.PpSlideShowType.ppShowTypeSpeaker;
sst.Run();
}
}
}
}
Запустите приложение и проверьте, что презентация открывается и запускается. Нам необходимо добавить возможность присоединяться к определённой мультикаст группе, слушать UDP и предсотавлять полученные данные программе. Motion API телефона даёт три угла поворота, вокруг соответствующих осей X, Y, Z, плюс, желательно знать время, когда эти данные получены. Это определяет формат класса PhoneMotion:
public class PhoneMotion
{
public double X;
public double Y;
public double Z;
public DateTime lastUpdateTime;
}
В качестве основы возмём класс TCP сервера из предыдущего примера:
using System; using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace PowerPointController
{
public class PhoneMotion
{
public double X;
public double Y;
public double Z;
public DateTime lastUpdateTime;
}
public class PhoneListener
{
internal class PhoneMotionControl
{
public PhoneMotionControl()
{
}
public PhoneMotion motion = new PhoneMotion();
public bool bStop = false;
}
public const int DefaultPort = 9978;
private string _ipAddress = string.Empty;
private int _port = DefaultPort;
private Thread _clientThread = null;
private PhoneMotionControl _motion = new PhoneMotionControl();
public string ServerIPAddress
{
get
{
return _ipAddress;
}
set
{
_ipAddress = value;
}
}
public int Port
{
get
{
return _port;
}
set
{
_port = value;
}
}
public PhoneMotion Motion
{
get
{
return GetMotion();
}
}
public PhoneListener()
{
}
public bool Start()
{
if (_clientThread != null && _motion.bStop != false)
return true;
_motion = new PhoneMotionControl();
_clientThread = new Thread(new ParameterizedThreadStart(PhoneThread));
_clientThread.Start(_motion);
while (string.IsNullOrEmpty(_ipAddress))
Thread.Sleep(100);
return true;
}
public bool Start(string ipaddress, int portnumber)
{
ServerIPAddress = ipaddress;
Port = portnumber;
return Start();
}
public void Stop()
{
_motion.bStop = true;
_clientThread = null;
}
public PhoneMotion GetMotion()
{
PhoneMotion m = new PhoneMotion();
lock(_motion)
{
m.X = _motion.motion.X;
m.Y = _motion.motion.Y;
m.Z = _motion.motion.Z;
m.lastUpdateTime = _motion.motion.lastUpdateTime;
} return m;
}
private void PhoneThread(object obj)
{
PhoneMotionControl m = (PhoneMotionControl)obj;
IPAddress localAddress = IPAddress.Loopback;
foreach(IPAddress ip in Dns.GetHostAddresses(Dns.GetHostName()))
{
if(ip.AddressFamily == AddressFamily.InterNetwork)
{
localAddress = ip;
break;
}
}
_ipAddress = localAddress.ToString();
while (!m.bStop)
{
TcpClient client = newsock.AcceptTcpClient();
if (client != null)
{
using (NetworkStream stream = client.GetStream())
{
while (!m.bStop)
{
byte[] values = new byte[sizeof(double) + 1];
int recv = stream.Read(values, 0, values.Length);
if (recv == values.Length)
{
lock (m.motion)
{
switch (values[0])
{
case 0:
m.motion.X = BitConverter.ToDouble(values, 1);
break;
case 1:
m.motion.Y = BitConverter.ToDouble(values, 1);
break;
case 2:
m.motion.Z = BitConverter.ToDouble(values, 1);
break;
default:
break;
}
m.motion.lastUpdateTime = DateTime.Now;
}
}
}
}
}
} newsock.Stop();
}
}
}
Фактически нам необходимо модифицировать только метод PhoneThread, оставив весь остальной код таким же, как и при работе с TCP. Для преемственнсти, сохраним формат передающегося сообщения с маркерами-байтами, хотя в случае с UDP сообщение должно прийти сразу всё. Кроме этого, UDP – протокол не требует соединения, однако, мультикаст соединения мы слушаем по локальному порту. Поэтому, необходимо создать UDP клиента, который привязан к локальному IP и порту, далее присоединиться к мультикаст гурппе и слушать групповые сообщения. В виде кода, это будет выглядеть следующим образом:
private void PhoneThread(object obj)
{
PhoneMotionControl m = (PhoneMotionControl)obj;
IPAddress localAddress = IPAddress.Loopback;
foreach(IPAddress ip in Dns.GetHostAddresses(Dns.GetHostName()))
{
if(ip.AddressFamily == AddressFamily.InterNetwork)
{
localAddress = ip;
break;
}
}
IPEndPoint localIP = new IPEndPoint(localAddress, _port);
UdpClient listener = new UdpClient(localIP);
listener.JoinMulticastGroup(IPAddress.Parse("224.7.9.1"));
IPEndPoint groupEP = new IPEndPoint(IPAddress.Any, _port);
_ipAddress = localAddress.ToString();
while (!m.bStop)
{ if (listener != null)
{ while (!m.bStop)
{
byte[] bytes = listener.Receive(ref groupEP);
lock (m.motion)
{
m.motion.X = BitConverter.ToDouble(bytes, 1);
m.motion.Y = BitConverter.ToDouble(bytes, 10);
m.motion.Z = BitConverter.ToDouble(bytes, 19);
m.motion.lastUpdateTime = DateTime.Now;
}
}
}
}
}
Обратите внимание, что в данном коде кроется потенциальная проблема. На строчке
byte[] bytes = listener.Receive(ref groupEP);
произойдёт блокировка потока PhoneThread и даже, если мы захотим его остановить, пока не прийдёт следующее сообщение, поток остановлен не будет. Также мы предполагаем, что мы всегда получаем полноее сообщение такого же формат, как использовалось в TCP примере. Также обратите внимание, что адрес мультикаст группы просто записан в коде. Это сделано для удобства представление его в формате блога, чтобы было удобнее копировать.
Также как и в примере с TCP соединением удобно будет добавить событие, оповещающее об изменении данных. Код будет немного отличаться, так как мы здесь получаем сразу все данные. Добавим в в пространсово имён следующий код:
public delegate void PhoneMotionEventHandler(
Object sender,
PhoneMotionChangedEventArgs e);
public class PhoneMotionChangedEventArgs
{
private PhoneMotion _pm;
public PhoneMotionChangedEventArgs(PhoneMotion pm)
{
_pm = pm;
}
public PhoneMotion Motion
{
get { return _pm; }
}
}
В сам класс добавим необходимые определения:
public event PhoneMotionEventHandler PhoneMotionChanged;
private void NotifyPhoneMotionChanged(PhoneMotion pm)
{
if (PhoneMotionChanged != null)
{
PhoneMotionChanged(this, new PhoneMotionChangedEventArgs(pm));
}
}
И генерацию самого события в код PhoneThread:
. . . .
while (!m.bStop)
{ if (listener != null)
{ while (!m.bStop)
{
byte[] bytes = listener.Receive(ref groupEP);
lock (m.motion)
{
m.motion.X = BitConverter.ToDouble(bytes, 1);
m.motion.Y = BitConverter.ToDouble(bytes, 10);
m.motion.Z = BitConverter.ToDouble(bytes, 19);
m.motion.lastUpdateTime = DateTime.Now;
NotifyPhoneMotionChanged(GetMotion());
}
}
}
}
. . . .
Теперь можно добавить инициализацию UDP сервера и обработку события в код приложения:
using System;
using System.Windows.Forms;
using System.IO;
using PowerPoint = Microsoft.Office.Interop.PowerPoint;
namespace PowerPointController
{
public partial class Main : Form
{
private string _pptFileName = "";
PowerPoint.Presentation presesntation;
PhoneListener pl;
const double PI3 = Math.PI/3;
DateTime timeToContinue;
const double SwitchDelay = 3;
public Main()
{
InitializeComponent();
}
private void SelectPPT_Click(object sender, EventArgs e)
{
OpenFileDialog ofd = new OpenFileDialog();
if (ofd.ShowDialog()==DialogResult.OK)
{
_pptFileName = ofd.FileName;
pl = new PhoneListener();
pl.Start();
}
}
private void RunPPT_Click(object sender, EventArgs e)
{
if (File.Exists(_pptFileName))
{ PowerPoint.Application application = new PowerPoint.Application();
presesntation = application.Presentations.Open(_pptFileName, Microsoft.Office.Core.MsoTriState.msoTrue);
PowerPoint.SlideShowSettings sst = presesntation.SlideShowSettings;
sst.ShowType = Microsoft.Office.Interop.PowerPoint.PpSlideShowType.ppShowTypeSpeaker;
sst.Run();
pl.PhoneMotionChanged += new PhoneMotionEventHandler(pl_PhoneMotionChanged);
timeToContinue = DateTime.Now;
}
}
void pl_PhoneMotionChanged(object sender, PhoneMotionChangedEventArgs e)
{
if ((e.Motion != null) &&
(DateTime.Now > timeToContinue))
{
if (e.Motion.X > PI3)
{
timeToContinue = timeToContinue.AddSeconds(SwitchDelay);
presesntation.SlideShowWindow.View.Next();
}
else
if (e.Motion.X < -PI3)
{
timeToContinue = timeToContinue.AddSeconds(SwitchDelay);
presesntation.SlideShowWindow.View.Previous();
}
}
}
}
}
Обратите внимание на то, что мы вынуждены приотанавливать обработку событий изменения поворота, чтобы переключать можно было традиционными “покачиваниями” телефона, иначе, у нас быстро презентация переключится до конца. Срабатывание происходит, когда по данным с телефона угло больше PI/3, т.е. 60 градусов.
У нас полностью готова серверная часть. Теперь осталось только передать нужные данные с телефона.
Воспользуемся шаблоном Windows Phone Application и создадим простое приложение:, кнопка “Присоединиться” и три labels для отображения углов поворота:
<!--TitlePanel contains the name of the application and page title-->
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="ApplicationTitle" Text="ПРЕЗЕНТЕР"
Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock x:Name="PageTitle" Text="настройка" Margin="9,-7,0,0"
Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<StackPanel>
<Button Name="btnConnect" Content="Присоединиться" Click="btnConnect_Click" />
<StackPanel Orientation="Vertical" Margin="24,48,0,0">
<TextBlock Name="tbX" Text="X: " FontSize="40" />
<TextBlock Name="tbY" Text="Y: " FontSize="40" />
<TextBlock Name="tbZ" Text="Z: " FontSize="40" />
<TextBlock Name="tbErr" Text=" " Height="92" TextWrapping="Wrap"/>
</StackPanel>
</StackPanel>
</Grid>
Добавим код, для присоедениения к мультикаст группе и рассылке сообщений о изменении положения телефона:
public partial class MainPage : PhoneApplicationPage
{
Motion m = new Motion(); double X, Y, Z;
double PX, PY, PZ;
const double rtog = 180 / Math.PI;
private const string GROUP_ADDRESS = "224.7.9.1";
private const int GROUP_PORT = 9978;
UdpAnySourceMulticastClient _client = null;
bool _joined = false;
// Constructor
public MainPage()
{
InitializeComponent();
}
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
m.CurrentValueChanged += new EventHandler<SensorReadingEventArgs<MotionReading>>(m_CurrentValueChanged);
m.Start();
}
void m_CurrentValueChanged(object sender, SensorReadingEventArgs<MotionReading> e)
{
X = e.SensorReading.Attitude.Pitch;
Y = e.SensorReading.Attitude.Roll;
Z = e.SensorReading.Attitude.Yaw; Dispatcher.BeginInvoke(() =>
{
tbX.Text = string.Format("X: {0}", (int)(X * rtog));
tbY.Text = string.Format("Y: {0}", (int)(Y * rtog));
tbZ.Text = string.Format("Z: {0}", (int)(Z * rtog)); PX = X;
PY = Y;
PZ = Z;
SendUpdate();
});
}
private void Join()
{
_client = new UdpAnySourceMulticastClient(IPAddress.Parse(GROUP_ADDRESS), GROUP_PORT); _client.BeginJoinGroup(
result =>
{
_client.EndJoinGroup(result);
_client.MulticastLoopback = false;
_joined = true;
}, null);
}
private void SendUpdate()
{
if (_joined)
{
byte[] b0 = new byte[1];
byte[] buffer = new byte[1]; byte[] bytesX = BitConverter.GetBytes(X);
byte[] bytesY = BitConverter.GetBytes(Y);
byte[] bytesZ = BitConverter.GetBytes(Z); b0[0] = 0;
buffer = b0.Concat(bytesX).ToArray();
b0[0] = 1;
buffer = buffer.Concat(b0).ToArray();
buffer = buffer.Concat(bytesY).ToArray();
b0[0] = 2;
buffer = buffer.Concat(b0).ToArray();
buffer = buffer.Concat(bytesZ).ToArray();
_client.BeginSendToGroup(buffer, 0, buffer.Length,
result =>
{
_client.EndSendToGroup(result);
}, null);
}
}
private void btnConnect_Click(object sender, RoutedEventArgs e)
{
Join();
}
Как видно, код для UDP гораздо проще. Нам не обязательно соединятся с конкретным хостом (при этом нам не гарантирована доставка), также, поскольку нам нет необходимости читать сообщения из группы, мы не реализуем получение сообщений и отключаем получение своих собтсвенных сообщений:
_client.MulticastLoopback = false;
Фактически код состоит из 2-х частей. Первая, это работа с Motion API по получению данных о положении устройства. Мы создаём объет Motion и подписываемся на изменения положения устройства (CurrentValueChanged) и все изменния послаем по сети серверу (метод SendUpdate). Чтобы данные начали пересылаться, необходимо сначала присоединиться к мультикаст группе (метод Join).
Для работы с мультикаст группами создаётся объект типа UdpAnySourceMulticastClient , которому указывается адрес группы и порт.
Чтобы переслать данные на сервер, формируется уже знакомый нам буфер с данными, размеченый байтами-маркерами и вызвается асинхронный метод передачи данных BeginSendToGroup.
Обратите внимание, что и в примере сервера UDP порт 9978 указан прямо в коде, если вас по каким-то причинам не устраивает этот порт - выделите его в качтсве параметра. Весь нефункциональный код обработки ошибок и проблемных кейсов (например, сервер просто не получает сообщение, а мы об этом даже не знаем) отсутсвует в примере. Для использования в реальных условиях код необходимо соответсвующим образом дополнить.