Переключение слайдов презентации с использованием Windows Phone: UDP соединение

В продолжение темы использования Windows Phone в качестве устройства переключения слайдов, представим себе гипотетическую ситуацию, когда сразу нам надо сразу на нескольоких ноутбуках переключать презентацию одновременно. Будем использовать для этого возможности UDP - Mulitcast Group, позволяющий разослать всем присоединившимся к групее сообщение.

Как обычно начнём с серверной части, которая будет слушать сообщения с телефона  и выполнять необходимые действия. Можно за основу взять приложение из предыдущей статьи Переключение слайдов презентации с использованием Windows Phone: TCP соединение, здесь же я напишу приложение “заново”.

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

Итак, приступим к созданию WinForm приложения. Это будет простая форма с двумя кнопками,. Одна кнопка для открытия файла презентации, другая для запуска презентации.

udp-article-1

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