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

Несколько лет назад я был на Windows Mobile Developers Day и докладчик из Microsoft показывал приложение, которое позволяло переключать слайды презентации с телефона на Windows Mobile. Приложение использовало Bluetooth соединение и, скорее всего, писало команды в COM-порт. Сейчас практически на всех конференциях есть WiFi, поэтому я решил написать простой пример приложения, которое бы позволяло управлять презентацией по сети. Кроме этого, я решил не искать лёгких путей, а использовать данные Motion API телефона, чтобы переключать слайды.

В этой статье я рассмотрю пример, когда нам нужно переключать слайды на одном компьютере и буду соединяться с ним по TCP соединению, используя возможности работы с TCP сокетами, предоставляемые Windows Phone.

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

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

tcp-wp-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();   
            }   
        }   
     }
}  

Запустите приложение и проверьте, что презентация открывается и запускается. Нам необходимо добавить возможность слушать по определённому адресу и порту TCP и сохранять данные. 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;
                }
            }   
             TcpListener newsock = new TcpListener(IPAddress.Any, _port); 
            newsock.Start();   
            _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();
        }   
     }
}  

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

 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 enum Axis
    {
        X,Y,Z
    }   
      public delegate void PhoneMotionEventHandler(
      Object sender,
      PhoneMotionChangedEventArgs e);   
     public class PhoneMotionChangedEventArgs
    {
        private PhoneMotion _pm;
        private Axis _axis;   
         public PhoneMotionChangedEventArgs(PhoneMotion pm, Axis axis)
        {
            _pm = pm;
            _axis = axis;
        }   
  public PhoneMotion Motion
        {
            get { return _pm; }
        }   
         public Axis Axis
        {
            get { return _axis; }
        }
    }   
      public class PhoneListener
    {
        internal class PhoneMotionControl
        {
            public PhoneMotionControl()
            {
            }   public PhoneMotion motion = new PhoneMotion();
            public bool bStop = false;
        }   
         public event PhoneMotionEventHandler PhoneMotionChanged;   
         private void NotifyPhoneMotionChanged(PhoneMotion pm, Axis axis)
        {
            if (PhoneMotionChanged != null)
            {
                PhoneMotionChanged(this, new PhoneMotionChangedEventArgs(pm, axis));
            }
        }   
         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;
                }
            }   
             TcpListener newsock = new TcpListener(IPAddress.Any, _port); 
            newsock.Start();   
             _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);
                                            NotifyPhoneMotionChanged(GetMotion(), Axis.X);
                                            break;
                                        case 1:
                                            m.motion.Y = BitConverter.ToDouble(values, 1);
                                            NotifyPhoneMotionChanged(GetMotion(), Axis.Y);
                                            break;
                                        case 2:
                                            m.motion.Z = BitConverter.ToDouble(values, 1);
                                            NotifyPhoneMotionChanged(GetMotion(), Axis.Z);
                                            break;
                                        default:
                                            break;
                                    }
                                    m.motion.lastUpdateTime = DateTime.Now;
                                }                                
                            }
                        }
                    }
                }
            }   newsock.Stop();
        }   }
}  

Осталось добавить инициализацию и старт TCP сервера и обработку события изменения положения телефона в код основной формы:

 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();
                
                 lblServerIP.Text = pl.ServerIPAddress;
                lblPort.Text = pl.Port.ToString();
            }
        }   
         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) && 
                (e.Axis == Axis.X) && 
                (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>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="IP адрес: " Margin="0,24,0,0"/>
            <TextBox Name="tbIP" Width="256" Height="80" VerticalAlignment="Top"/>
        </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>

Теперь напишем код, который будет соединяться с сервером по указаному адресу и слать данные о положении телефона.

     Motion m = new Motion();   
     double X, Y, Z;
    double PX, PY, PZ;
    double smoothing = 0.35;   
     bool bConnected = false;
    Socket _socket = null;   
     const double rtog = 180 / Math.PI;   
    // Constructor
     public MainPage()
    {
        InitializeComponent();
    }   
     private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
    {
        if (IsolatedStorageSettings.ApplicationSettings.Contains("ServerName"))
            tbIP.Text = IsolatedStorageSettings.ApplicationSettings["ServerName"].ToString().Trim();   
         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();
        });
    }   
     void SendUpdate()
    {
        if (!bConnected || !_socket.Connected)
            return;   
         byte[] b0 = new byte[1];
        
         byte[] buffer = new byte[1];   
         SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();   
         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();   
         socketEventArg.RemoteEndPoint = _socket.RemoteEndPoint;
        socketEventArg.UserToken = null;   
         socketEventArg.SetBuffer(buffer, 0, buffer.Length);
        _socket.SendAsync(socketEventArg);
    }   
     private void btnConnect_Click(object sender, RoutedEventArgs e)
    {
        if (string.IsNullOrEmpty(tbIP.Text.Trim()))
            return;   if(bConnected)
            return;   tbErr.Text = string.Empty;   
         _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);   
         SocketAsyncEventArgs socketEventArg = new SocketAsyncEventArgs();   
         if (Char.IsNumber(tbIP.Text.Trim()[0]))
        {
            IPEndPoint hostEntry = new IPEndPoint(IPAddress.Parse(tbIP.Text.Trim()), 9978);
            socketEventArg.RemoteEndPoint = hostEntry;
        }
        else
        {
            DnsEndPoint ep = new DnsEndPoint(tbIP.Text.Trim(), 9978, AddressFamily.InterNetwork);
            socketEventArg.RemoteEndPoint = ep;
        }   
         socketEventArg.Completed += new EventHandler<SocketAsyncEventArgs>(delegate(object s, SocketAsyncEventArgs ev)
        {
            if (ev.SocketError == SocketError.Success)
            {
                bConnected = true;
                Dispatcher.BeginInvoke(() =>
                    {
                        btnConnect.IsEnabled = false;
                        IsolatedStorageSettings.ApplicationSettings["ServerName"] = tbIP.Text.Trim();
                        IsolatedStorageSettings.ApplicationSettings.Save();
                    });
            }
            else
            {
                Dispatcher.BeginInvoke(() =>
                {
                    tbErr.Text = string.Format("{0}", ev.SocketError.ToString());
                });
            }
        });   _socket.ConnectAsync(socketEventArg);            
    }
}

Фактически код состоит из 2-х частей. Первая, это работа с Motion API по получению данных  о положении устройства.  Мы создаём объет Motion и подписываемся на изменения положения устройства (CurrentValueChanged) и все изменния послаем по сети серверу (метод SendUpdate). Чтобы данные начали пересылаться, необходимо сначала соедниться с сервером (метод btnConnect_Click) для чего считываются данные из поля ввода и выполняется попытка соединения по указаному адресу; в случае успеха, кнопка отключается.

Для соединения создаётся объект типа Socket, котороу указывается тип сокета (Stream) и протокол (TCP), после чего, при необходимости разрешается ввденый адрес и формируется IPEndPoint, который используется в параметрах соединения (SocketAsyncEventArgs). Далее просто вызывается асинхронный метод соединения (ConnectAsync), результат работы которого обрабатывается в inline обработчике.

Чтобы переслать данные на сервер, формируется уже знакомый нам класс SocketAsyncEventArgs, в который в добавляется буфер с данными, размеченый байтами-маркерами и вызвается асинхронный метод передачи данных SendAsync.

Обратите внимание, что и в примере сервера и в примерре клиента TCP порт 9978 указан прямо в коде, если вас по каким-то причинам не устраивает этот порт  - выделите его в качтсве параметра. Весь  нефункциональный код обработки ошибок и проблемных кеёсов (например, если клиент или сервер отвалится) отсутсвует в примере. Для использования в реальных условиях код необходимо соответсвующим образом дополнить.

Надеюсь, вам понравился пример. В нём есть ещё много чего для доработки. Так что мне и вам, дорогие читатели, будет чем заняться в новогодние каникулы.

В залючение, хочу поблагодарить коллегу Владмира Колесникова, за предоставление основного кода, работающего с TCP.