-
Notifications
You must be signed in to change notification settings - Fork 75
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
zephyr/modules: Add "sensor dashboard" sample web app.
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
Showing
5 changed files
with
239 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
R.py: | ||
mpy_bin2res.py static/* >$@ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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});', | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.