Super Simple Dashboarding Directly on the Linkit One
by JustinN1 in Circuits > Microcontrollers
2081 Views, 9 Favorites, 0 Comments
Super Simple Dashboarding Directly on the Linkit One
This started as a want to sort of a port of this idea from an intel edison project I started: Intel Edison Sensor Dashboard Using Freeboard and Python
It is also sort of an augmentation of this instructable: https://www.instructables.com/id/Linkit-One-Serving-JSONP-sensor-data-battery-examp/
When I started writing the above instructable I didn’t like how I had to run freeboard on my laptop. Part of the beauty of hosting freeboard on the edison and also using that as a sensor platform meant that I could have a one stop shop for visualization and sensor ingest. All that was needed to have a custom view of the data is a device with a web browser.
So, I started to wonder if I could do the same thing with the linkit one. It has plenty of power and plenty of storage space for a small js page like freeboard. The one thing I was missing was a web server framework. On the Edison this was easy. I pick the Sinatra-like (http://www.sinatrarb.com/) framework that made the most sense to me (I chose Flask http://flask.pocoo.org/ because I wanted to use python and it was simple enough.)
Back to the linkit one. There isn’t something exactly like that so I did it a little differently.
These tutorials/threads in particular were very helpful when learning about serving web content with an arduino/linkit one: http://forum.arduino.cc/index.php?topic=167184.0 http://embeddedcoolness.com/faq/00500-ajax-web-server/
I thought implementing a simple dashboard using someone else’s widgets would be easy, but as you will see I ran into a number of problems and ended up making this very simple.
Materials
The linkit one kit - we will use the battery, the linkitone board, the GPS antenna (not really, but it is another sensor that you could show on here), and the wifi antenna
A good text editor - I don’t like the arduino IDE for this
The Arduino IDE for uploading sketches to the linkit one
A Page About Failure
Here are some things I tried...that did not work. These are rabbit holes that I got into and almost led me to give up.
-First I attempted to load all of the freeboard js pages and all of its dependencies normally and just feed them directly to the client by reusing wifiwebserver and code from a well known arduino forum member, zoomkat. This led me to come up with something like what you see here:
/* Written for the Linkit One
* A webserver that looks for index.html in the root dir of the onboard storage * and serves that page... * I am using this to test hosting freeboard for a few projects * Thanks to the user zoomkat on the arduino forums, * and the WifiWebServer and Storage example code from the linkit one team */#include <LTask.h> #include <LWifi.h> #include <LWifiClient.h> #include <LWifiServer.h> #include <LFlash.h> #include <LSD.h> #include <LStorage.h>
#define WIFI_AP "blah" #define WIFI_PASSWORD "blah" #define WIFI_AUTH LWIFI_WPA
#define Drv LFlash
LWiFiServer server(80);
String readString, pos;
void setup() { pinMode(10, OUTPUT);
Drv.begin(); LWiFi.begin(); Serial.begin(115200);
// keep retrying until connected to AP Serial.println("Connecting to AP"); while (0 == LWiFi.connect(WIFI_AP, LWiFiLoginInfo(WIFI_AUTH, WIFI_PASSWORD))) { delay(1000); }
printWifiStatus();
Serial.println("Start Server"); server.begin(); Serial.println("Server Started"); }
int loopCount = 0;
void loop() { // put your main code here, to run repeatedly: delay(500); loopCount++; LWiFiClient client = server.available(); if (client) { Serial.println("new client"); // an http request ends with a blank line boolean currentLineIsBlank = true; while (client.connected()){ if (client.available()){ // we basically ignores client request, but wait for HTTP request end char c = client.read();
//read char by char HTTP request if (readString.length() < 100) { //store characters to string readString += c; Serial.print(c); } //if HTTP request has ended if (c == '\n'){ Serial.println("readstring"); Serial.println(readString); if (readString.indexOf("freeboard") >=0){ client.println("HTTP/1.1 200 OK"); //send new page client.println("Content-Type: text/html"); client.println(); LFile myFile = Drv.open("index.html"); if (myFile) { while (myFile.available()) { client.write(myFile.read()); } myFile.close(); } } else { //if the file exists continue String filepath = readString.substring( (readString.indexOf("GET")+5),(readString.indexOf("HTTP")) ); char fullpath[1024]; filepath.toCharArray(fullpath,1024); Serial.println(filepath); String extension = filepath.substring( filepath.lastIndexOf(".") ); Serial.println(extension); if (true){ Serial.println("send response"); // send a standard http response header client.println("HTTP/1.1 200 OK"); if ((extension.indexOf("html") >=0) || (extension.indexOf("htm") >= 0) ){ client.println("Content-Type: text/html"); } if (extension.indexOf("css") >=0){ client.println("Content-Type: text/css"); }
if (extension.indexOf("js") >=0){ client.println("Content-Type: application/javascript"); } if (extension.indexOf("map") >=0){ client.println("Content-Type: application/x-navimap"); } if (extension.indexOf("png") >=0){ client.println("Content-Type: image/png"); } client.println(); LFile myFile = Drv.open(fullpath); if (myFile) { while (myFile.available()) { client.write(myFile.read()); } myFile.close(); } } //if the file doesn't exist bail with 404 else{ client.println("HTTP/1.1 404"); client.println("Content-Tyep: text/html"); client.println(); client.println("404 file not found"); } } delay(100); // close the connection: Serial.println("close connection"); client.stop(); } } } }
}
void printWifiStatus() { // print the SSID of the network you're attached to: Serial.print("SSID: "); Serial.println(LWiFi.SSID());
// print your WiFi shield's IP address: IPAddress ip = LWiFi.localIP(); Serial.print("IP Address: "); Serial.println(ip);
Serial.print("subnet mask: "); Serial.println(LWiFi.subnetMask());
Serial.print("gateway IP: "); Serial.println(LWiFi.gatewayIP());
// print the received signal strength: long rssi = LWiFi.RSSI(); Serial.print("signal strength (RSSI):"); Serial.print(rssi); Serial.println(" dBm"); } For now we will focus on this part: if (myFile) { while (myFile.available()) { client.write(myFile.read()); } myFile.close(); } }
What happens here is that as the file stream comes off of the memory space on the linkit one it gets put in a stream buffer in RAM and then from that buffer dumped into the outgoing network stack queues. This presents two issues:
That first buffer is actually pretty tiny and reading a file from the ROM is not particularly fast. So, what you end up with are very slow loading pages. Sometimes they time out or do other weird things. I wiresharked (https://www.wireshark.org/) the connection and it looks like it only spits out tiny packets of around 50 bytes (later this started to make sense to me as the buffer used when reading files in is probably about that size). Freeboard actually has a bunch of dependencies that it needs to load. Some of these do concurrent things and dynamically load stuff. This doesn’t work in a simple linear program. There are probably ways to make this work on an arduino but not easily (a start would be to look here: https://learn.adafruit.com/multi-tasking-the-ardu... .
I tried using various methods of reading the file in and making sure the buffer was almost full before sending more data to the client. but didn’t get very far. I also tried readUntil and it’s variants. None of these did the trick
Some assorted links I perused during this rabbit hole exploration:
http://forum.arduino.cc/index.php?topic=279849.msg...
http://playground.arduino.cc/Code/WebServerST
http://forum.arduino.cc/index.php?topic=279849.msg...
https://www.arduino.cc/en/Reference/StreamReadStr...
Finally I broke down and looked for something very lightweight to satisfy my desire for a graphical representation of data along with the json exposure of the data. What I came up with rellies on the html 5 “meter” and/or “progress” elements. These are essentially tiny bar graphs that render without external .js page includes or heavyweight styling. These are bare bones.
The page code I came up with looked something like this(and was read by something similar to the listing above, but with an addition to present the battery readings as json at /bat):
<!DOCTYPE html>
<html> <body> <h3>Simple Sensor Meters Demo</p> <p>battery(using meter element): <meter id="bat" min="0" max="100" value="0"></meter></p> <p>battery(using progress element): <progress id="batp" max="100" value="0"></meter></p> <p>charging: <h3 id="charging"></h3></p><script>
function Get(yourUrl){ var Httpreq = new XMLHttpRequest(); // a new request Httpreq.open("GET",yourUrl,false); Httpreq.send(); return Httpreq.responseText; } function updateMeters() { var jsonBat = JSON.parse(Get("/bat")); window.alert(jsonBat); document.getElementById("bat").value = jsonBat.batteryLevel; document.getElementById("batp").value = jasonBat.batteryLevel; document.getElementById("charging").innerHTML = jsonBat.chargingStatus; } updateMeters(); setInterval(updateMeters,5000); </script> </body> </html>
However, I ran into another issue with my code…
If I were to load an html page and that html page would try to do an HTTP GET at something on the linkit one then nothing would happen. I was perplexed until I figured it out. The code was single threaded and blocking. So, it was waiting until the initial page was done loading to load the next page. This never happened and so it got stuck.
Finally a Simple Solution
This is what I finally came up with. Something super simple. No javascript at all. No dependencies, no loading external json pages, just simple html with a refresh tag. It isn’t the prettiest thing in the world, but it works.
Most of the walkthrough is equivalent to what is in https://www.instructables.com/id/Linkit-One-Serving-JSONP-sensor-data-battery-examp/ So, I won’t repeat that all here.
I will however highlight some things that you should check out:
<p><meta http-equiv="refresh" content="30" /></p>
This forces the page to reload every 30 seconds. It is so small that you will not notice.
String batteryLevel = String(LBattery.level());
String batteryCharging = String(LBattery.isCharging());
I convert the integers from the LBattery functions directly into Strings for ease of printing.
The <meter> tag does not render on all browsers. For instance, in Safari on my wife's iphone it renders as blank.
Also, as you see in the comments this is based on Apache licensed code from Evothings. Please don't copy and pasted the code boxes in this example. Some escape characters were used (This tool was pretty helpful in making things print correctly for this instructable: http://www.accessify.com/tools-and-wizards/developer-tools/quick-escape/default.php). Instead download the code from my github here: https://github.com/stirobot/evothings-examples/blob/master/examples/mediatek-linkit/mediatek_linkit/linkitone_battery_json_with_meters.ino
The listing:
//
// Copyright 2015, Evothings AB // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // LinkIt One - Position // // Created February, 2015 // // Modified by github user stirobot in November, 2015 to expose battery status instead of GPS (for a simpler example) // This example shows you how to fetch the position and share it using // a simple webserver. //#include <LTask.h> #include <LWifiServer.h> #include <LWifiClient.h> #include <LWifi.h> #include <LBattery.h>
// Configuration of the WiFi AP settings. #define WIFI_AP "blah" #define WIFI_PASSWORD "black"
// LWIFI_OPEN, LWIFI_WPA, or LWIFI_WEP. #define WIFI_AUTH LWIFI_WPA
// Configure the timeout of a http request (ms). const uint32_t requestTimeout = 1000;
// Global variables LWiFiServer server(80); char buff[256];
void setup() { LTask.begin(); LWiFi.begin(); Serial.begin(115200); //delay(2000); }
void loop() { static bool wifiStatusPrinted = false;
connectToAccessPoint(); //straightforward
if (Serial && wifiStatusPrinted == false) { printWifiStatus(); wifiStatusPrinted = true; } else if(!Serial) { wifiStatusPrinted = false; }
LWiFiClient client = server.available();
if (client) { Serial.println("new client"); // an http request ends with a blank line boolean currentLineIsBlank = true; uint32_t lastReceptionTime = millis(); while (client.connected()) { if (client.available()) { // we basically ignores client request, but wait for HTTP request end int c = client.read(); lastReceptionTime = millis();
Serial.print((char)c); String batteryLevel = String(LBattery.level()); String batteryCharging = String(LBattery.isCharging()); if (c == '\n' && currentLineIsBlank) { Serial.println("send response"); // send a standard http response header client.println("HTTP/1.1 200 OK"); client.println("Content-Type: text/html"); client.println("Connection: close"); // the connection will be closed after completion of the response client.println("Access-Control-Allow-Origin: *"); client.println();
client.println("<!DOCTYPE html>"); client.println("<html><body>"); client.println("<h3>Simple Sensor Meters Demo</p>"); client.println("<p>battery(using meter element): " + batteryLevel + "<meter id=\"bat\" min=\"0\" max=\"100\" value=\"" + batteryLevel + "\"></meter></p>"); client.println("<p>battery(using progress element): " + batteryLevel + "<progress id=\"batp\" min=\"0\" max=\"100\" value=\"" + batteryLevel + "\"></meter></p>"); client.println("<p>charging: " + batteryCharging + "</p>"); client.println("<meta http-equiv=\"refresh\" content=\"30\" /></body></html>"); client.println(); break;
}
if (c == '\n') { // you're starting a new line currentLineIsBlank = true; } else if (c != '\r') { // you've gotten a character on the current line currentLineIsBlank = false; } } else { if (millis() - lastReceptionTime > requestTimeout) { Serial.println("Error - client timeout, dropping connection..."); break; } } } // give the web browser time to receive the data delay(500);
// close the connection: Serial.println("close connection"); client.stop(); Serial.println("client disconnected"); } }
// Helper functions
void connectToAccessPoint() {
while (LWiFi.status() != LWIFI_STATUS_CONNECTED) { if (LWiFi.connect(WIFI_AP, LWiFiLoginInfo(WIFI_AUTH, WIFI_PASSWORD))) { server.begin(); printWifiStatus(); } else { Serial.println("Error - failed to connect to WiFi"); } } }
// Helper functions from the WifiWebServer.ino example developed by MediaTek void printWifiStatus() { // print the SSID of the network you're attached to: Serial.print("SSID: "); Serial.println(LWiFi.SSID());
// print your WiFi shield's IP address: IPAddress ip = LWiFi.localIP(); Serial.print("IP Address: "); Serial.println(ip);
Serial.print("subnet mask: "); Serial.println(LWiFi.subnetMask());
Serial.print("gateway IP: "); Serial.println(LWiFi.gatewayIP());
// print the received signal strength: long rssi = LWiFi.RSSI(); Serial.print("signal strength (RSSI):"); Serial.print(rssi); Serial.println(" dBm\n"); }