Saturday, September 19, 2015

NodeJS/Heroku Clock server for the ESP8266 module

Somewhere I would like to visit..

So from my last post, I got my ESP8266 up and running. The next step is to implement something into the http web server that allows setting time intervals for when the relay is enabled/disabled. The ESP8266 doesn't have a real time clock component so the ESP8266 doesn't actually know real absolute time other than since when it started. Therefore, I need a way to keep track of time. Luckily, it does have access to the internet so I can just poll network time. I decided not to go with the standard method of NTP because the API is rather difficult to find, not to mention the example sketch that polls time/date from NTP servers looks frightening... and lengthy. Getting a network time is easy, you can just google "Vancouver time" and google has an in-line widget that shows you the time of Vancouver. In fact, thousands of websites actually give you time from different cities. However, performing a GET request from the ESP8266 using these sites doesn't seem like the best of ideas since the returned HTML from the HTTP requests are rather large compared to the 96kb of data ram available on the ESP8266. Since I'm using a gateway RESTful server that handles outbound sms text messages, I can just create an auxiliary service that filters time and outputs as simple JSON response which should reduce the size of these web sites (46kb in my case using timeanddate.com) down to less than 300 bytes. That should be much easier for the ESP8266 modules to gobble down.

The NodeJS server requires a valid country and city string as how timeanddate.com is used. (eg. http://www.timeanddate.com/ worldclock/canada/vancouver). This converts to /clock/canada/vancouver on the Heroku app. On parsing, it uses NodeJS' request package to create a GET request to timeanddate website.

app.get('/clock/:country/:city', function (req, res) {
  var options = { method: 'GET'};
  options.url = "http://www.timeanddate.com/worldclock/" + req.params.country + "/" + req.params.city;
  
  request(options, function (error, response, body) {
    if (error) throw new Error(error);

      /* Parse the date and time into a JSON object */
      var r = parseDateAndTime(body);
      
      res.setHeader("Content-type", "application/json");
      res.send(JSON.stringify(r));
  });
});

With the html response of the timeanddate.com webpage, the html is DOM-parsed using NodeJS' cheerio package (as called from above code snippet parseDateAndTime())... without the date yet.

function parseDateAndTime(timeAndDateHTML){
        /* Let cheerio parse the html */
      $ = cheerio.load(timeAndDateHTML);
  
      /* Create response object */
      var r = {};
      
      /* Save the time as a text as-is for response */
      r.time = $('#ct').text();
      
      var timeString = r.time.toLowerCase().trim();
      
      /* Determine if this is am or pm */
      var isPM = timeString.indexOf("pm");
      
      /* remove am/pm string */
      t = timeString.replace("pm", "").replace("am", "").trim();
      
      /* split the time string using ':' */
      var timeSplitted = t.split(":");
      
      /* Get the hour in 24-format, min, and seconds*/
      r.hour = parseInt(timeSplitted[0]);
      if (isPM) r.hour = r.hour + 12;
      r.min = parseInt(timeSplitted[1]);
      r.sec = parseInt(timeSplitted[2]);
      
      return r;
}

On the Arduino side, the code is now relatively simple using WifiClient and ArduinoJson library.

  // Has to be of type const char *
  const char *host = "esp8266-clock.herokuapp.com"; 

  // Use WiFiClient class to create TCP connections
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort)) {
    Serial.println("connection failed");
    return;
  }

  // Make the GET request
  client.print("GET /clock/canada/vancouver HTTP/1.1\r\nHost: esp8266-clock.herokuapp.com\r\n");

  delay(10);
  
  String res = "";

  // Read all the lines of the reply from server and print them to Serial
  while(client.available()){
    String line = client.readStringUntil('\r');
    Serial.print(line);
    res += line;
  }

  /* Convert String to char array */
  char json[line.length()];
  line.toCharArray(json, line.length());


  /* Parse the JSON time */
  StaticJsonBuffer<300> jsonBuffer;

  JsonObject& root = jsonBuffer.parseObject(json);

  const char* timeString = root["time"];
  byte hour              = root["hour"];
  byte min               = root["min"];
  byte sec               = root["sec"];

2 comments :