Skip to content

Commit

Permalink
zephyr/modules: Add "sensor dashboard" sample web app.
Browse files Browse the repository at this point in the history
This implements a simple single-threaded web server, serving an HTML/JS
application and "sensor" data via server-side events. Instead of real
sensor data, this example uses pseudo-random numbers.
  • Loading branch information
pfalcon committed Feb 24, 2018
1 parent c898f73 commit 49b157e
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 0 deletions.
2 changes: 2 additions & 0 deletions ports/zephyr/modules/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
R.py:
mpy_bin2res.py static/* >$@
4 changes: 4 additions & 0 deletions ports/zephyr/modules/R.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
R = {
'static/dashboard.html': b'<!DOCTYPE html>\n<html>\n<head>\n\n<style>\n.grid {\n position: relative;\n}\n.item {\n display: table;\n table-layout: fixed;\n\n// position: absolute;\n width: 200px;\n height: 200px;\n margin: 5px;\n z-index: 1;\n border-style: solid;\n border-color: #e7e7e7;\n}\n.item-content {\n position: relative;\n border-color: black;\n\n display: table-cell;\n vertical-align: middle;\n text-align: center;\n}\n\n</style>\n\n<script src="static/easypiechart.min.js"></script>\n\n<script>\nvar source = new EventSource("events");\n\nfunction clear_errors() {\n document.getElementById("messages").innerHTML = "";\n}\n\nfunction log_error(error) {\n document.getElementById("messages").innerHTML = "EventSource error:" + error + "<br>";\n}\n\nfunction set_val(widget, field, val) {\n document.querySelectorAll("." + widget).forEach(function(el) {\n el.querySelector("[data-bind=" + field + "]").innerHTML = val;\n });\n}\n\nsource.onmessage = function(event) {\n clear_errors();\n\n var data = JSON.parse(event.data);\n// console.log(data);\n\n set_val(data.widget, data.field, data.value);\n}\n\nsource.onerror = function(error) {\n console.log(error);\n log_error(error);\n}\n</script>\n</head>\n<body>\n\n<div class="item">\n<div class="item-content">\n\n<div class="w_text">\n <h1 class="title" data-bind="title">Hello</h1>\n <h3 data-bind="text">This is your shiny new (MicroPython powered) dashboard.</h3>\n</div>\n\n</div>\n</div>\n\n<div class="item">\n<div class="item-content">\n\n<div class="widget w_value">\n val1=<span data-bind="value1"></span><br/>\n val2=<span data-bind="value2"></span>\n</div>\n\n</div>\n</div>\n\n<!-- Gauge -->\n<div class="item">\n<div class="item-content">\n\n<div class="w_piegauge">\n <span class="chart" data-percent="73"><span class="percent">73%</span></span>\n <style>\n.chart {\n display: flex;\n justify-content: center;\n align-items: center;\n}\n.chart canvas {\n position: absolute;\n}\n.percent {\n position: absolute;\n z-index: 2;\n}\n\n </style>\n <script>\n var element = document.querySelector(\'.chart\');\n var chart = new EasyPieChart(element, {\n scaleColor: false,\n lineWidth: 10,\n onStep: function(from, to, percent) {\n this.el.children[0].innerHTML = Math.round(percent);\n }\n });\n\n source.addEventListener("message", function(event) {\n var data = JSON.parse(event.data);\n if (data.field === "value2") {\n chart.update(data.value);\n }\n });\n\n </script>\n</div>\n\n</div>\n</div>\n\n<div id="messages"></div>\n</div>\n\n</body>\n</html>\n',
'static/easypiechart.min.js': b'/**!\n * easy-pie-chart\n * Lightweight plugin to render simple, animated and retina optimized pie charts\n *\n * @license \n * @author Robert Fleischmann <rendro87@gmail.com> (http://robert-fleischmann.de)\n * @version 2.1.7\n **/\n!function(a,b){"function"==typeof define&&define.amd?define([],function(){return a.EasyPieChart=b()}):"object"==typeof exports?module.exports=b():a.EasyPieChart=b()}(this,function(){var a=function(a,b){var c,d=document.createElement("canvas");a.appendChild(d),"object"==typeof G_vmlCanvasManager&&G_vmlCanvasManager.initElement(d);var e=d.getContext("2d");d.width=d.height=b.size;var f=1;window.devicePixelRatio>1&&(f=window.devicePixelRatio,d.style.width=d.style.height=[b.size,"px"].join(""),d.width=d.height=b.size*f,e.scale(f,f)),e.translate(b.size/2,b.size/2),e.rotate((-0.5+b.rotate/180)*Math.PI);var g=(b.size-b.lineWidth)/2;b.scaleColor&&b.scaleLength&&(g-=b.scaleLength+2),Date.now=Date.now||function(){return+new Date};var h=function(a,b,c){c=Math.min(Math.max(-1,c||0),1);var d=0>=c?!0:!1;e.beginPath(),e.arc(0,0,g,0,2*Math.PI*c,d),e.strokeStyle=a,e.lineWidth=b,e.stroke()},i=function(){var a,c;e.lineWidth=1,e.fillStyle=b.scaleColor,e.save();for(var d=24;d>0;--d)d%6===0?(c=b.scaleLength,a=0):(c=.6*b.scaleLength,a=b.scaleLength-c),e.fillRect(-b.size/2+a,0,c,1),e.rotate(Math.PI/12);e.restore()},j=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(a){window.setTimeout(a,1e3/60)}}(),k=function(){b.scaleColor&&i(),b.trackColor&&h(b.trackColor,b.trackWidth||b.lineWidth,1)};this.getCanvas=function(){return d},this.getCtx=function(){return e},this.clear=function(){e.clearRect(b.size/-2,b.size/-2,b.size,b.size)},this.draw=function(a){b.scaleColor||b.trackColor?e.getImageData&&e.putImageData?c?e.putImageData(c,0,0):(k(),c=e.getImageData(0,0,b.size*f,b.size*f)):(this.clear(),k()):this.clear(),e.lineCap=b.lineCap;var d;d="function"==typeof b.barColor?b.barColor(a):b.barColor,h(d,b.lineWidth,a/100)}.bind(this),this.animate=function(a,c){var d=Date.now();b.onStart(a,c);var e=function(){var f=Math.min(Date.now()-d,b.animate.duration),g=b.easing(this,f,a,c-a,b.animate.duration);this.draw(g),b.onStep(a,c,g),f>=b.animate.duration?b.onStop(a,c):j(e)}.bind(this);j(e)}.bind(this)},b=function(b,c){var d={barColor:"#ef1e25",trackColor:"#f9f9f9",scaleColor:"#dfe0e0",scaleLength:5,lineCap:"round",lineWidth:3,trackWidth:void 0,size:110,rotate:0,animate:{duration:1e3,enabled:!0},easing:function(a,b,c,d,e){return b/=e/2,1>b?d/2*b*b+c:-d/2*(--b*(b-2)-1)+c},onStart:function(a,b){},onStep:function(a,b,c){},onStop:function(a,b){}};if("undefined"!=typeof a)d.renderer=a;else{if("undefined"==typeof SVGRenderer)throw new Error("Please load either the SVG- or the CanvasRenderer");d.renderer=SVGRenderer}var e={},f=0,g=function(){this.el=b,this.options=e;for(var a in d)d.hasOwnProperty(a)&&(e[a]=c&&"undefined"!=typeof c[a]?c[a]:d[a],"function"==typeof e[a]&&(e[a]=e[a].bind(this)));"string"==typeof e.easing&&"undefined"!=typeof jQuery&&jQuery.isFunction(jQuery.easing[e.easing])?e.easing=jQuery.easing[e.easing]:e.easing=d.easing,"number"==typeof e.animate&&(e.animate={duration:e.animate,enabled:!0}),"boolean"!=typeof e.animate||e.animate||(e.animate={duration:1e3,enabled:e.animate}),this.renderer=new e.renderer(b,e),this.renderer.draw(f),b.dataset&&b.dataset.percent?this.update(parseFloat(b.dataset.percent)):b.getAttribute&&b.getAttribute("data-percent")&&this.update(parseFloat(b.getAttribute("data-percent")))}.bind(this);this.update=function(a){return a=parseFloat(a),e.animate.enabled?this.renderer.animate(f,a):this.renderer.draw(a),f=a,this}.bind(this),this.disableAnimation=function(){return e.animate.enabled=!1,this},this.enableAnimation=function(){return e.animate.enabled=!0,this},g()};return b});',
}
86 changes: 86 additions & 0 deletions ports/zephyr/modules/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
try:
import usocket as socket
except:
import socket
import utime
import urandom

from R import R


def resp_header(resp, content_type):
resp.write("HTTP/1.0 200 OK\r\n")
resp.write("Content-Type: %s\r\n" % content_type)
resp.write("\r\n")


def handle(resp, path):
if path == b"/":
resp_header(resp, "text/html")
resp.write(R["static/dashboard.html"])

elif path.startswith(b"/static/"):
if path.endswith(b".js"):
resp_header(resp, "text/html")
else:
resp_header(resp, "application/javascript")
resp.write(R[str(path[1:], "utf-8")])

elif path == b"/events":
resp_header(resp, "text/event-stream")

i = 0
try:
while True:
resp.write('data: {"widget": "w_value", "field": "value1", "value": "%s"}\n\n' % urandom.getrandbits(7))
if i & 1:
resp.write('data: {"widget": "w_value", "field": "value2", "value": "%s"}\n\n' % urandom.getrandbits(7))
utime.sleep(1)
i += 1
except OSError:
print("Event source connection closed")
return

else:
assert 0, path


def main():
s = socket.socket()

# Binding to all interfaces - server will be accessible to other hosts!
ai = socket.getaddrinfo("0.0.0.0", 8080)
print("Bind address info:", ai)
addr = ai[0][-1]

s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen(5)
print("Listening, connect your browser to http://<this_host>:8080/")

while True:
res = s.accept()
client_sock = res[0]
client_addr = res[1]
print("Client address:", client_addr)
print("Client socket:", client_sock)

client_stream = client_sock

print("Request:")
req = client_stream.readline()
method, path, proto = req.split(None, 2)
print(method, path, proto)
while True:
h = client_stream.readline()
if h == b"" or h == b"\r\n":
break
print(h)

handle(client_stream, path)

client_stream.close()
print()


main()
138 changes: 138 additions & 0 deletions ports/zephyr/modules/static/dashboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<!DOCTYPE html>
<html>
<head>

<style>
.grid {
position: relative;
}
.item {
display: table;
table-layout: fixed;

// position: absolute;
width: 200px;
height: 200px;
margin: 5px;
z-index: 1;
border-style: solid;
border-color: #e7e7e7;
}
.item-content {
position: relative;
border-color: black;

display: table-cell;
vertical-align: middle;
text-align: center;
}

</style>

<script src="static/easypiechart.min.js"></script>

<script>
var source = new EventSource("events");

function clear_errors() {
document.getElementById("messages").innerHTML = "";
}

function log_error(error) {
document.getElementById("messages").innerHTML = "EventSource error:" + error + "<br>";
}

function set_val(widget, field, val) {
document.querySelectorAll("." + widget).forEach(function(el) {
el.querySelector("[data-bind=" + field + "]").innerHTML = val;
});
}

source.onmessage = function(event) {
clear_errors();

var data = JSON.parse(event.data);
// console.log(data);

set_val(data.widget, data.field, data.value);
}

source.onerror = function(error) {
console.log(error);
log_error(error);
}
</script>
</head>
<body>

<div class="item">
<div class="item-content">

<div class="w_text">
<h1 class="title" data-bind="title">Hello</h1>
<h3 data-bind="text">This is your shiny new (MicroPython powered) dashboard.</h3>
</div>

</div>
</div>

<div class="item">
<div class="item-content">

<div class="widget w_value">
val1=<span data-bind="value1"></span><br/>
val2=<span data-bind="value2"></span>
</div>

</div>
</div>

<!-- Gauge -->
<div class="item">
<div class="item-content">

<div class="w_piegauge">
<span class="chart" data-percent="73"><span class="percent">73%</span></span>
<style>
.chart {
display: flex;
justify-content: center;
align-items: center;
}
.chart canvas {
position: absolute;
}
.percent {
position: absolute;
z-index: 2;
}

</style>
<script>
var element = document.querySelector('.chart');
var chart = new EasyPieChart(element, {
scaleColor: false,
lineWidth: 10,
onStep: function(from, to, percent) {
this.el.children[0].innerHTML = Math.round(percent);
}
});

source.addEventListener("message", function(event) {
var data = JSON.parse(event.data);
if (data.field === "value2") {
chart.update(data.value);
}
});

</script>
</div>

</div>
</div>

<div id="messages"></div>
</div>

</body>
</html>
9 changes: 9 additions & 0 deletions ports/zephyr/modules/static/easypiechart.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 49b157e

Please sign in to comment.