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:
- You have to buy an extra remote controller.
- The remote controller might be lost when you really want to use it.
- Your remote controller might be out of battery charge when doing presentation.
- You might walk to the audience and then go out of the control area.
The Remofi has the following advantages:
- Almost everyone has a smart phone, so you can control your PPT with this phone without worrying about no controller.
- 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:
public MicroServerCore(IPAddress ipAddr, int port)
{
listener = new TcpListener(ipAddr, port);
serverThread = new Thread(() =>
{
listener.Start();
while (true)
{
Socket s = listener.AcceptSocket();
NetworkStream ns = new NetworkStream(s);
StreamReader sr = new StreamReader(ns);
HttpRequest req = new HttpRequest(sr);
HttpResponse resp = ProcessRequest(req);
StreamWriter sw = new StreamWriter(ns);
sw.WriteLine("HTTP/1.1 {0}", resp.StatusText);
sw.WriteLine("Content-Type: " + resp.ContentType);
sw.WriteLine("Content-Length: {0}", resp.Data.Length);
sw.WriteLine("Cache-Control: no-cache");
sw.WriteLine();
sw.Flush();
s.Send(resp.Data);
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.codeproject.com' 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:
- The HTTP '
get
' method - The path '
/
' - 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:
private HttpResponse ProcessRequest(HttpRequest req)
{
StringBuilder sb = new StringBuilder();
if (req.Url.EndsWith("down"))
{
PPTAction.ControlPPT(ActionType.down);
}
else if (req.Url.EndsWith("up"))
{
PPTAction.ControlPPT(ActionType.up);
}
else
{
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.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/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