Controlling a Raspberry Pi with a Cell Phone

In the previous project, I implemented PWM on a Raspberry Pi using direct hardware access.

In this project, I will implement a web-app to control the PWM from a browser running on a phone.

The first step is to write an HTTP server to open a TCP/IP socket and send requested files to the client browser. The code to do that is listed below.

//
// server.c
//

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <ctype.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/stat.h>
#include <sys/mman.h>

#include "Globals.h"
#include "utils.c"
#include "socket.c"
#include "rpiModel.c"
#include "rpiMemMap.c"
#include "rpiPwm.c"

#define PORT 12345 // A random number > 1024

#define ISspace(x) isspace((int)(x))

#define SERVER_STRING "Server: foohttpd/0.1.0\r\n"

static UInt32
getNumber(char **sp)
{
  char *s = *sp;
  UInt32 n = strtol(s, (char **) &s, 10);
  if (*s != ',') {
    fatal("Missing comma in LED cmd");
  }
  *sp = s + 1;
  return n;
}

static void
doLedCmd(char *cmd)
{
  Assert(cmd[0] == 'L');
  Assert(cmd[1] == ',');
  char *s = &cmd[2];
  UInt32 redFreq       = getNumber(&s);  Assert(s[-1] == ',');
  UInt32 redDutyCycle  = getNumber(&s);  Assert(s[-1] == ',');
  UInt32 blueFreq      = getNumber(&s);  Assert(s[-1] == ',');
  UInt32 blueDutyCycle = getNumber(&s);  Assert(s[-1] == ',');
  Assert(s == (cmd + strlen(cmd)));
  pwmSet(redFreq, redDutyCycle, blueFreq, blueDutyCycle);
}

static void
doCommand(char *cmd)
{
  // epf("doCommand(%s)", cmd);
  switch (cmd[0]) {
  case 'L': doLedCmd(cmd); break;

  default:
    epf("Bad AJAX: {%s}", cmd);
    return;
  }
  /*
    const int PWM = 1023;
    FIXME
    FIXME
    FIXME
    if (cmd[0] != 'D') {
    setPwmA(0);
    setPwmB(0);
    Assert(cmd[0] == 'U');
    return;
    }
    Assert(cmd[3] == '\0');
    switch (cmd[1]) {
    case 'F': setPwmB(PWM); setMotorDirectionB(0); break;
    case 'R': setPwmB(PWM); setMotorDirectionB(1); break;
    case '0': setPwmB(0);   break;
    default:  Assert(0);
    }
    switch (cmd[2]) {
    case 'F': setPwmA(PWM); setMotorDirectionA(1); break;
    case 'R': setPwmA(PWM); setMotorDirectionA(0); break;
    case '0': setPwmA(0);   break;
    default:  Assert(0);
    }
   */
}

static void
unimplemented(int clientSocket)
{
  const char msg[] =
    "HTTP/1.0 501 Method Not Implemented\r\n"
    SERVER_STRING
    "Content-Type: text/html\r\n"
    "\r\n"
    "<HTML><HEAD><TITLE>Method Not Implemented\r\n"
    "</TITLE></HEAD>\r\n"
    "<BODY><P>HTTP request method not supported.\r\n"
    "</BODY></HTML>\r\n";
  send(clientSocket, msg, strlen(msg), 0);
}

static void
notFound(int clientSocket)
{
  const char msg[] =
    "HTTP/1.0 404 NOT FOUND\r\n"
    SERVER_STRING
    "Content-Type: text/html\r\n"
    "\r\n"
    "<HTML><TITLE>Not Found</TITLE>\r\n"
    "<BODY><P>The server could not fulfill\r\n"
    "your request because the resource specified\r\n"
    "is unavailable or nonexistent.\r\n"
    "</BODY></HTML>\r\n";
  send(clientSocket, msg, strlen(msg), 0);
}

static const char *
lookupContentType(const char *filename)
{
  static const char *tab[][2] = {
    { "css",  "text/css" },
    { "html", "text/html" },
    { "ico",  "image/vnd.microsoft.icon" },
    { "js",   "text/javascript" },
    { "txt",  "text/plain" },
  };
  const char *ext = strrchr(filename, '.');
  if (ext == NULL) {
    return NULL;
  }
  Assert(*ext == '.');
  ++ext;
  for (int i = 0; i < (int) (sizeof(tab)/sizeof(tab[0])); ++i) {
    if (strcmp(ext, tab[i][0]) == 0) {
      return tab[i][1];
    }
  }
  return NULL;
}

static void
sendHeader(int clientSocket, const char *filename)
{
  char buf[4096];
  const char msg[] =
    "HTTP/1.0 200 OK\r\n"
    SERVER_STRING
    "Content-Type: %s\r\n"
    "\r\n";
  const char *ct = lookupContentType(filename);
  if (ct == NULL) {
    epf("Unknown content type for %s", filename);
    ct = "text/html";
  }
  sprintf(buf, msg, ct);
  send(clientSocket, buf, strlen(buf), 0);
}

static void
discardHeader(int clientSocket)
{
  int numchars;
  char buf[1024];
  do {
    numchars = getLineFromSocket(clientSocket, buf, sizeof(buf));
  } while ((numchars > 0) && (strcmp("\n", buf) != 0));
}

static void
sendFile(int clientSocket, const char *filename)
{
  char buf[0x10000];
  int fd = open(filename, O_RDONLY);
  int w, r;
  if (fd < 0) {
    notFound(clientSocket);
    return;
  }
  sendHeader(clientSocket, filename);
  for (;;) {
    r = read(fd, buf, sizeof(buf));
    if (r < 0) {
      fatal("Read() failed: %s", syserr());
    }
    if (r == 0) {
      close(fd);
      return;
    }
    w = send(clientSocket, buf, r, 0);
    if (w != r) {
      fatal("Write failed (%d != %d) : %s", w, r, syserr());
    }
  }
}

static void
processRequest(int clientSocket)
{
  char buf[4096];
  char path[1024];
  unsigned int n = getLineFromSocket(clientSocket, buf, sizeof(buf));
  Assert(n == strlen(buf));
  discardHeader(clientSocket);
  if (strncmp(buf, "GET ", 4) != 0) {
    unimplemented(clientSocket);
    return;
  }
  char *p = buf + 4;
  char *url = p;
  Assert(*url > ' ');  
  while (*p > ' ') {
    ++p;
  }
  Assert(*p == ' ');
  *p = '\0';
  if (strncmp(url, "/doCommand?cmd=", 15) == 0) {
    url += 15;
    doCommand(url);
    url = "/ok.txt";
  }
  sprintf(path, "htdocs%s", url);
  if (path[strlen(path) - 1] == '/') {
    strcat(path, "index.html");
  } else if (isDir(path)) {
    strcat(path, "/index.html");
  }
  // epf("path={%s}", path);
  sendFile(clientSocket, path);
  return;
}

static int
getServerSocket(void)
{
  struct sockaddr_in name;
  int httpd = socket(PF_INET, SOCK_STREAM, 0);
  if (httpd == -1) {
    fatal("socket() failed: %s", syserr());
  }
  if (setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR,
		 &(int){1}, sizeof(int)) < 0) {
    fatal("setsockopt() failed: %s", syserr());
  }
  memset(&name, 0, sizeof(name));
  name.sin_family = AF_INET;
  name.sin_port = htons(PORT);
  name.sin_addr.s_addr = htonl(INADDR_ANY);
  if (bind(httpd, (struct sockaddr *)&name, sizeof(name)) < 0) {
    fatal("bind() failed: %s", syserr());
  }
  if (listen(httpd, 5) < 0) {
    fatal("listen() failed: %s", syserr());
  }
  return httpd;
}

int
main(void)
{
  g.modelNum = rPiModelNumber();
  int serverSocket = getServerSocket();
  epf("httpd running on port %d", PORT);
  pwmInit();
  pwmSet(1, 1, 1, 1);
  for (;;) {
    struct sockaddr_in client_name;
    unsigned int client_name_len = sizeof(client_name);
    int clientSocket = accept(serverSocket,
			      (struct sockaddr *)&client_name,
			      &client_name_len);
    if (clientSocket == -1) {
      fatal("accept() failed: %s", syserr());
    }
    processRequest(clientSocket);
    close(clientSocket);
  }
  close(serverSocket);
  return 0;
}

We also need some utility functions for dealing with the socket requests. These are listed below:

//
//  socket.c
//

static int
isDir(const char *path)
{
  struct stat st;
  if (stat(path, &st) == -1) {
    return 0;
  }
  return (st.st_mode & S_IFMT) == S_IFDIR;
}


static int
getOneByteFromSocket(int sock, int flag)
{
  char c = 0;
  int r = recv(sock, &c, 1, flag);
  if (r &lt; 0) {
    fatal("recv failed: %s", syserr());
  }
  Assert((r == 0) || (r == 1));
  return c;
}

static int
getLineFromSocket(int sock, char *buf, int size)
{
  char *p = buf;
  while ((p - buf) &lt; (size - 1)) {
    char c;
    c = getOneByteFromSocket(sock, 0);
    if (c == 0) {
      break;
    }
    if (c == '\r') {
      c = getOneByteFromSocket(sock, MSG_PEEK);
      if (c == '\n') {
	c = getOneByteFromSocket(sock, 0);
      }
      c = '\n';
    }
    *p++ = c;
    if (c == '\n') {
      break;
    }
  }
  *p = '\0';
  return p - buf;
}

Next, we need the HTML and JavaScript files to create the user interface in the phone's browser.

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>LED PWM</title>
    <link rel="stylesheet" href="/style.css" type="text/css" />  
  </head>
  <body>
    <div id="mainDiv">
      <div id="redDiv">
	<div>
	  <span>Freq:</span>
	  <label id="redFreqLbl" class="lbl">1</label>
	</div>
	<input type="range" min="1" max="100" value="0" class="slider" id="redFreqSlider" />
	<div>
	  <span>Width:</span>
	  <label id="redWidthLbl" class="lbl">0</label>
	</div>
	<input type="range" min="0" max="100" value="0" class="slider" id="redWidthSlider" />
      </div>

      <div id="blueDiv">
	<div>
	  <span>Freq:</span>
	  <label id="blueFreqLbl" class="lbl">1</label>
	</div>
	<input type="range" min="1" max="100" value="0" class="slider" id="blueFreqSlider" />
	<div>
	  <span>Width:</span>
	  <label id="blueWidthLbl" class="lbl">0</label>
	</div>
	<input type="range" min="0" max="100" value="0" class="slider" id="blueWidthSlider" />
      </div>
    </div>
    <script src="main.js"></script>
  </body>
</html>
//
//  main.js
//

document.addEventListener('DOMContentLoaded', function(event) {
    'use strict';

    var g = {};
	    
    async function sendCmd() {
	var xhttp = new XMLHttpRequest();
	var cmd = 'L,' +
	    g.redFreqLbl.innerHTML   + ',' +
	    g.redWidthLbl.innerHTML  + ',' +
	    g.blueFreqLbl.innerHTML  + ',' +
	    g.blueWidthLbl.innerHTML + ',';
	    
	console.log('{' + cmd + '}');
	    
	var url = 'doCommand?cmd=' + cmd;
	xhttp.open('GET', url, true);
	xhttp.send();
    }

    function preventDefaults() {
	document.body.addEventListener('touchmove',function(e) {
	    e = e || window.event;
	    var target = e.target || e.srcElement;
	    //in case $altNav is a class:
	    if (!target.className.match(/\baltNav\b/)) {
		e.returnValue = false;
		e.cancelBubble = true;
		if (e.preventDefault) {
		    e.preventDefault();
		    e.stopPropagation();
		}
		return false;//or return e, doesn't matter
	    }
	},false);
    }
	    
    function setupSliders() {
	function getId(s) { return document.getElementById(s); }

	preventDefaults();
	    
	g.redFreqSlider   = getId('redFreqSlider');
	g.redWidthSlider  = getId('redWidthSlider');
	g.blueFreqSlider  = getId('blueFreqSlider');
	g.blueWidthSlider = getId('blueWidthSlider');

	g.redFreqLbl   = getId('redFreqLbl');
	g.redWidthLbl  = getId('redWidthLbl');
	g.blueFreqLbl  = getId('blueFreqLbl');
	g.blueWidthLbl = getId('blueWidthLbl');
	    
	g.redFreqSlider.oninput   = function() { g.redFreqLbl.innerHTML   = this.value; sendCmd(); };
	g.redWidthSlider.oninput  = function() { g.redWidthLbl.innerHTML  = this.value; sendCmd(); };
	g.blueFreqSlider.oninput  = function() { g.blueFreqLbl.innerHTML  = this.value; sendCmd(); };
	g.blueWidthSlider.oninput = function() { g.blueWidthLbl.innerHTML = this.value; sendCmd(); };
    }

    console.log('Starting up, DOM loaded (001)');
    setupSliders();
});

Ok. Now let's run it. A normal HTTP server is on port 80, but that port may already be in use, so we can use port 12345 instead. It could be any unused port number, as long as the server and client agree. We also need the internet address of the Raspberry Pi. To get that we runhostname -I.

$ hostname -I 192.168.0.115 $ $
PWM control with a phone

So we start the server on the Raspberry Pi, and then connect over Wifi by entering the URLhttp://192.168.0.115:12345/.

When the interface is displayed, we can used the sliders to control the frequency and the pulse width (duty cycle) of the signal driving the LEDs. Click on the video to see a demo.