Author Topic: Streaming data from the Oak  (Read 7255 times)

digi_guy

  • Jr. Member
  • **
  • Posts: 87
Streaming data from the Oak
« on: December 18, 2016, 10:28:37 am »
Last year I posted a thread about how to stream data from the Digispark Pro , here is an updated version that works with the Oak. The key difference from last time is that I figured out how to bundle all the data into one package.

This is known as server-sent-events, which is a nice middle ground between having the client side polling repeatedly for data, or having a full on websocket

The idea is that, when requested, the server will keep the connection alive, and then send data without waiting for a request. The client side will have event listeners that call functions when data is sent.

The key piece of code is
Code: [Select]
var source = new EventSource("/stream"); which starts the process on the client side.

The next key piece is the event listener
Code: [Select]
source.addEventListener('pinA0', function(e) {var data = JSON.parse(e.data);

The event is called 'pinA0' and it passes the event data 'e' to a function when ever the server sends that information.

On the server side we respond to the streaming request with
Code: [Select]
    client.println(F("HTTP/1.1 200 OK"));
    client.println(F("Content-Type: text/event-stream"));
    client.println(F("Cache-Control: no-cache"));
    client.println(F("Connection: keep-alive"));
    client.println();

And then whenever we want to send information it absolutely has to be in a very specific format, another else crashes the client (by design).
Code: [Select]
event: pinA0 \n
data: {"msg": "hello world", "id": 12345}\n\n
where in this case 'pinA0' is the name of our event, and the data is the information we want to send.

This example will plot the analog reading from A0 on a simple graph vs up-time, and also print the data to the console.log

What sucks is that I'm using a simple for-loop because at the moment I don't know a better way. If anyone has any suggestions let me know, I'm going to keep playing around. I need a way to close then restart the function, right now I have to reboot the oak to start again.

When adapting this to your own project keep two things in mind:
1. the event name needs to match both the client side and server side, so in my case I use pinA0, which will call a function on the client side. you can have as many events as you like, calling as many functions as you like ,just make sure they match. My previous example had two events that were reporting data from two pins.

2. you can pile as much info as you want into each event, simply using the "name: value" pairing. the client side uses JSON.parse to break that into "e.name = value" within javascript.

Any tips or suggestions would be greatly appreciated.



Code: [Select]
#include <ESP8266WiFi.h>

//SYSTEM_MODE(SEMI_AUTOMATIC);
//Particle.connect();

WiFiServer server(80);  //the port that requests are made on
WiFiClient client;

int analogPin = A0;     // potentiometer wiper (middle terminal) connected to analog pin 3
unsigned long lastTime = 0UL;
char publishTemp[40];
char publishTime[40];


void setup() {
  server.begin();  //starts listening for requests
  Particle.publish("status","awake");
  }


void loop() {
     
  // listen for incoming clients
  client = server.available();
  if (client) {

    if (client.connected()) {
      client.find("GET ");
      String path = client.readStringUntil(' ');
      Particle.publish("status","connected");
      if (path == "/stream"){   //First time through this will be false and oak will send the main webpage your broswer will send /stream and initiate streaming
        while (client.read() !=-1); // empty the buffer
        sendResponseStartStream();
       
                 for (int k=0; k<600; k++){
                // you can change this loop to make it run longer or shorter or change it to while (true) and have it run forever

                // now is in milliseconds
                  unsigned long now = millis();
                  unsigned nowSec = now/1000UL;
                  unsigned sec = nowSec%60;
                  unsigned min = (nowSec%3600)/60;
                  unsigned hours = (nowSec%86400)/3600;
                  sprintf(publishTime,"%u:%u:%u",hours,min,sec);
           
                sendStream("pinA0", "{\"TempC\":"+String(analogRead(analogPin))+", \"Time\": "+String(nowSec)+"}");   
                // There are two features here: The first is the event name "pinA0" which needs to match on the client side, that will be linked to a function, in this case it plots the graph
                // you can have as many of these events as you want, each calling a different javascript function
                // within each event is the data you want to send, in this case a dictionary of name/value pairs, the client side uses JSON.parse to make use of it
               
                while (Serial.read() !=-1); // empty the buffer
                delay(500); // change this to update faster or slower
                }
               

        sendResponseEnd();
      }
      else {
        sendResponseStart();
        mainPage();
        sendResponseEnd();
      }
    }
    // close the connection:
    client.stop();
  }
}

bool serverRequest(){
  if(client.available()>4){
    return client.find("GET ");
  }
  return false;
}

String getRequestPath(){
    String path = client.readStringUntil(' ');
    while(client.read() != -1); //clear read buffer
    return path;
}
void sendResponse(String response){
    sendResponseStart();
    sendResponseChunk(response);
    sendResponseEnd();
}



void sendResponseStart(){
    //sends a chunked response
    client.println(F("HTTP/1.1 200 OK"));
    client.println(F("Content-Type: text/html"));
    client.println(F("Connection: close"));
    client.println(F("Transfer-Encoding: chunked"));
    client.println();
}
void sendResponseChunk(String response){
    client.println(response.length()+2,HEX);
    client.println(response);
    client.println();
}

void sendResponseEnd(){
    client.println(F("0"));
    client.println();
}

void sendResponseStartStream(){  //This variation on the html response is required for streaming, and keeps the connection alive
    //sends the start of the stream response
    client.println(F("HTTP/1.1 200 OK"));
    client.println(F("Content-Type: text/event-stream"));
    client.println(F("Cache-Control: no-cache"));
    client.println(F("Connection: keep-alive"));
    client.println();
}


void sendStream(String event, String text) {  //This function sends the streaming data in the appropriate format
    //sends the data of the stream response
  String msg = "event: " + event + "\ndata: " + text + "\n\n";
  client.print(msg);                   
}


void mainPage() {
      sendResponseChunk(F("<!DOCTYPE html><html><body>"));
      sendResponseChunk(F("<canvas id=\"myCanvas\" width=\"800\" height=\"400\" style=\"border:1px solid #d3d3d3;\"></canvas>"));  //This draws the graph
      sendResponseChunk(F("<h1>Getting server updates</h1>"));
      sendResponseChunk(F("<div id=\"pinA0\">pinA0 goes here</div>"));
      sendResponseChunk(F("<div id=\"result\"></div>"));
      sendResponseChunk(F("<div id=\"onclose\"></div>"));
      sendResponseChunk(F("<div id=\"onopen\"></div>"));       
      sendResponseChunk(F("<script>"));
      sendResponseChunk(F("var c = document.getElementById(\"myCanvas\");"));
      sendResponseChunk(F("var ctx = c.getContext(\"2d\");"));
      sendResponseChunk(F("ctx.translate(0, 400);"));
      sendResponseChunk(F("ctx.font = \"10px Arial\";"));
      sendResponseChunk(F("ctx.scale(1,.25);"));   
      sendResponseChunk(F("if(typeof(EventSource) !== \"undefined\") {"));
      sendResponseChunk(F("var source = new EventSource(\"/stream\");"));      //this initiates streaming
      sendResponseChunk(F("source.onopen = function(event) {"));
      sendResponseChunk(F("var today=new Date();"));       
      sendResponseChunk(F(" document.getElementById(\"onopen\").innerHTML = \"opened: \"+today+ \"<br>\"; }; "));     
      sendResponseChunk(F("source.onerror = function(event) {"));
      sendResponseChunk(F("var today=new Date();"));       
      sendResponseChunk(F(" document.getElementById(\"onclose\").innerHTML = \"closed: \"+today+ \"<br>\"; }; "));     
      sendResponseChunk(F("source.addEventListener(\'pinA0\', function(e) {var data = JSON.parse(e.data); ")); 
      //This is the function that gets called each time the oak sends data, notice that the name pinA0 needs to match the event we defined earlier
      //you can have multiple
      sendResponseChunk(F("console.log(\"Temp: \"+data.TempC+\" Time: \"+data.Time); ctx.fillText(\"*\", data.Time, -data.TempC); "));
      sendResponseChunk(F("document.getElementById(\"pinA0\").innerHTML = data.TempC;}, false);"));     
      sendResponseChunk(F("} else {document.getElementById(\"result\").innerHTML = \"fail\";}"));
      sendResponseChunk(F("</script></body></html>"));
}