分享

Remofi

 quasiceo 2015-01-15
UI picture

Introduction

This article illustrates a way to control your computer program through your mobile WIFI device like a mobile phone. You don't have to install any software on the mobile device, but rely on its web browser. What you need to do is to launch the Remofi on your PC which you want to control. The Remofi will listen to a port you choose (by default, it's 1688, a lucky number the Chinese people love most), and then by visiting the IP address/port you choose, you can control your PowerPoint.

Background

The present PowerPoint controller has the following drawbacks:

  1. You have to buy an extra remote controller.
  2. The remote controller might be lost when you really want to use it.
  3. Your remote controller might be out of battery charge when doing presentation. 
  4. You might walk to the audience and then go out of the control area.

The Remofi has the following advantages:

  1. Almost everyone has a smart phone, so you can control your PPT with this phone without worrying about no controller.
  2. Wifi covers much larger room than infrared ray or blue-tooth device, so you can control your PPT even outside of the meeting room.

Using the Code

Remofi is essentially a micro web server. You can imagine it as a micro IIS, which listens to a port and serves a simple web page. Your web browser visits this page, and clicks the HTML button to sends out Ajax request to this micro web server to control the PPT. Not very complex, huh?

The core of Remofi is a class called 'MicroServerCore', whose constructor is:

/// <span class="code-SummaryComment"><summary>
</span>/// Initializes a new instance of the "MicroServerCore" class.
/// <span class="code-SummaryComment"></summary>
</span>/// <span class="code-SummaryComment"><param name="ipAddr">The ip address</param>
</span>/// <span class="code-SummaryComment"><param name="port">The port number</param>
</span>public MicroServerCore(IPAddress ipAddr, int port)
{
    listener = new TcpListener(ipAddr, port);

    //launch a thread to listen to the port
    serverThread = new Thread(() =>
    {
        listener.Start();
        while (true)
        {
            Socket s = listener.AcceptSocket();
            //Raw data received.
            NetworkStream ns = new NetworkStream(s);
            //Stream reader to 'interpert' it
            StreamReader sr = new StreamReader(ns);
            //Extracts data needed to construct an HttpReqeust object
            HttpRequest req = new HttpRequest(sr);
            //Determines to render a page or to execute command
            HttpResponse resp = ProcessRequest(req);
            StreamWriter sw = new StreamWriter(ns);
            //Write response stream
            sw.WriteLine("HTTP/1.1 {0}", resp.StatusText);
            sw.WriteLine("Content-Type: " + resp.ContentType);
            sw.WriteLine("Content-Length: {0}", resp.Data.Length);
            //Prevents the Ajax request being cached
            sw.WriteLine("Cache-Control: no-cache");
            sw.WriteLine();
            sw.Flush();
            s.Send(resp.Data);
            //Close the connection
            s.Shutdown(SocketShutdown.Both);
            ns.Close();
        }
    });
    serverThread.Start();
}

Here we can see that once the MicroServercore is instanced, there would be a thread launched to listen to the IP address and port you choose. This thread would not stop because of the while(true) loop inside of it, unless this thread is Stopped manually (the Stop method would also be called when the program Window is destroyed).

Once there's an HTTP request sent to this address, the request would be wrapped by an HttpRequest class, which is implemented as the following:

public class HttpRequest
{
    public string Method
    {
        get;
        private set;
    }
    public string Url
    {
        get;
        private set;
    }

    public string Protocol
    {
        get;
        private set;
    }

    public HttpRequest(StreamReader sr)
    {
        var s = sr.ReadLine();
        string[] ss = s.Split(' ');
        Method = ss[0];
        Url = (ss.Length > 1) ? ss[1] : "NA";
        Protocol = (ss.Length > 2) ? ss[2] : "NA";
    }
}

A typical HTTP request, for example, when we input 'http://www.' in the browser, on the IIS server, the first-line of this request would be like:

GET / HTTP/1.1

Notice that it's composed of three parts:

  1. The HTTP 'get' method
  2. The path '/'
  3. The protocol

You may wonder where the '/' path comes from, and the answer is that it's normalized when your browser submits your input into the server.

And in the constructor of this 'HttpRequest' class, we can see that we only read the first-line of the stream. In fact there's more information, but we don't need it.

After the HttpRequest is constructed, it would be processed by the 'ProcessRequest' function, which is like:

/// <span class="code-SummaryComment"><summary>
</span>/// Processes the request.
/// <span class="code-SummaryComment"></summary>
</span>/// <span class="code-SummaryComment"><param name="req">The req.</param>
</span>/// <span class="code-SummaryComment"><returns></returns>
</span>private HttpResponse ProcessRequest(HttpRequest req)
{
    StringBuilder sb = new StringBuilder();
    //If you click the 'down' button, then it sends a 
    //GET request like 'http://192.168.0.1:1688/down'
    if (req.Url.EndsWith("down"))
    {
        PPTAction.ControlPPT(ActionType.down);
    }
    // http://www.192.168.0.1/up
    else if (req.Url.EndsWith("up"))
    {
        PPTAction.ControlPPT(ActionType.up);
    }
    else
    {
        // if not requests like this, renders the page
        Assembly _assembly = Assembly.GetExecutingAssembly();
        StreamReader sr = new StreamReader
		(_assembly.GetManifestResourceStream("Remofi.HtmlSource.txt"));
        string tempString = string.Empty;

        while (!string.IsNullOrEmpty(tempString = sr.ReadLine()))
        {
            sb.Append(tempString);
        }
    }
    return new HttpResponse()
    {
        ContentType = "text/html",
        Data = Encoding.UTF8.GetBytes(sb.ToString())
    };
}

Here we can see that Remofi determines whether you want to show the web page button, or want to 'PageUp' the PowerPoint or want to 'PageDown' the PowerPoint by the URL you provided:

Notice that the action method to control the PowerPoint is wrapped in the PPTAction class. The static 'ControlPPT class would first determine if the PowerPoint window exists. If not, a warning message will pop up.

The actual method to control the PPT relies on a method called SendKeys(). It's explained in detail in the following MSDN link, so I won't expand it here:

Then it comes to the instantiation of the HttpResponse class:

    public string StatusText
    {
        get;
        set;
    }
    public string ContentType
    {
        get;
        set;
    }
    public byte[] Data
    {
        get;
        set;
    }
    public HttpResponse()
    {
        StatusText = "200 OK";
        ContentType = "text/plain";
        Data = new byte[] { };
    }
}

Notice that after the instantiation, when we're stuffing the network stream, we have such an HTTP header:

sw.WriteLine("Cache-Control: no-cache");

This header is very important, because if the request is cached, the actual code to control the behavior of the PowerPoint, would not be hit. And if you comment out the header accidentally in the code, you need to empty your browser cache to make it work again.

What's on the Client Side?

The client UI is very simple at present. In the Internet Explorer browser, it's like:

And on the iOS device, they're two round buttons.

When you visit the address you select, the actual HTML code rendered is:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www./1999/xhtml">
<head>
    <title>Remofi</title>
    <style type="text/css">
        #btnUp {
            height: 100px;
            width: 100px;
        }
        #btnDown
        {
            height: 100px;
            width: 100px;
        }
    </style>
		<script type="text/javascript">
			function buttonUp(){
			var xmlhttp;
			xmlhttp=new XMLHttpRequest();
			xmlhttp.open("GET","/up","true");
			xmlhttp.send();
		}
			function buttonDown(){
			var xmlhttp;
			xmlhttp=new XMLHttpRequest();
			xmlhttp.open("GET","/down","true");
			xmlhttp.send();
		}
</script>
</head>
<body>
    <p>
        <input id="btnUp" type="button" value="UP" onclick="buttonUp()" />
        <br />
        <input id="btnDown"
            type="button" value="Down" onclick="buttonDown()" /></p>
</body>
</html>

When you click the Html button in the browser, you actually launch an Ajax request to the Remofi, and the advantage of this is that your browser would not be refreshed.

function buttonUp(){
var xmlhttp;
xmlhttp=new XMLHttpRequest();
xmlhttp.open("GET","/up","true");
xmlhttp.send();

Points of Interest

The client UI is extremely simple, with only two buttons. The reason is that the page is shown on the mobile device, and two buttons here can do the demonstration. Of course, I do want to make it more colorful in the next update.

Another thing I need to tell you guys is that when I actually try to control the PPT when the PowerPoint is in full-screen mode, the control from the client browser would always force the PowerPoint to roll back to its preview mode.

I found that you can utilize some office DLL to realize the control when it's in full-screen mode, instead of the SendKeys method, but this way would compromise the extensibility of this program (because I think you can also use this way to control your other devices like your Media Center). So if anyone has a better idea, please let me know.

History

  • 2011.8.11: First Remofi prototype

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约