Initial server
This commit is contained in:
commit
73c3152e21
9 changed files with 336 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
heartbeat.db
|
||||||
|
venv/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
|
9
bin/server
Executable file
9
bin/server
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
import sys, os
|
||||||
|
sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/..')
|
||||||
|
|
||||||
|
from heartbeat import server
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
args = {}
|
||||||
|
server.start(args)
|
0
heartbeat/__init__.py
Normal file
0
heartbeat/__init__.py
Normal file
30
heartbeat/export.py
Normal file
30
heartbeat/export.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# TODO: web frontend
|
||||||
|
import os
|
||||||
|
import tornado.web
|
||||||
|
|
||||||
|
class ExportHandler(tornado.web.RequestHandler):
|
||||||
|
|
||||||
|
def initialize(self, conn):
|
||||||
|
"""
|
||||||
|
conn: sqlite3 connection
|
||||||
|
"""
|
||||||
|
self.conn = conn
|
||||||
|
|
||||||
|
def get(self, range):
|
||||||
|
# TODO: userid + start time
|
||||||
|
c = self.conn.cursor()
|
||||||
|
self.set_header("Content-Type", 'text/csv')
|
||||||
|
ranges = {
|
||||||
|
'1h': '-1 hour',
|
||||||
|
'24h': '-1 day',
|
||||||
|
'week': '-7 days',
|
||||||
|
}
|
||||||
|
if range not in ranges:
|
||||||
|
# Not found
|
||||||
|
raise tornado.web.HTTPError(404)
|
||||||
|
|
||||||
|
c.execute("SELECT * FROM beats WHERE createdAt > date('now', ?) ORDER BY createdAt ASC", (ranges[range],))
|
||||||
|
self.write("id,bpm,timestamp\n")
|
||||||
|
for row in c:
|
||||||
|
self.write("{},{},{}\n".format(row['id'], row['bpm'], row['createdAt']))
|
||||||
|
self.flush()
|
25
heartbeat/frontend.py
Normal file
25
heartbeat/frontend.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# TODO: web frontend
|
||||||
|
import os
|
||||||
|
import tornado.web
|
||||||
|
import tornado.template
|
||||||
|
|
||||||
|
|
||||||
|
viewdir = os.path.dirname(os.path.realpath(__file__)) + '/../views'
|
||||||
|
loader = tornado.template.Loader(viewdir)
|
||||||
|
|
||||||
|
class FrontendHandler(tornado.web.RequestHandler):
|
||||||
|
|
||||||
|
def initialize(self, conn):
|
||||||
|
"""
|
||||||
|
conn: sqlite3 connection
|
||||||
|
"""
|
||||||
|
self.conn = conn
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
# c = self.conn.cursor()
|
||||||
|
# c.execute("SELECT * FROM beats ORDER BY createdAt DESC LIMIT 20")
|
||||||
|
# beats = [row for row in c]
|
||||||
|
# print(beats)
|
||||||
|
# html = loader.load("sockets.html").generate(beats=beats)
|
||||||
|
html = loader.load("plot.html").generate()
|
||||||
|
self.write(html)
|
38
heartbeat/server.py
Normal file
38
heartbeat/server.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# import the libraries
|
||||||
|
import tornado.web
|
||||||
|
import tornado.ioloop
|
||||||
|
from .ws import WebSocketHandler
|
||||||
|
from .frontend import FrontendHandler
|
||||||
|
from .export import ExportHandler
|
||||||
|
import sqlite3
|
||||||
|
import os
|
||||||
|
|
||||||
|
def start(args):
|
||||||
|
basedir = os.path.dirname(os.path.realpath(__file__)) +'/../'
|
||||||
|
conn = sqlite3.connect(basedir + 'heartbeat.db')
|
||||||
|
|
||||||
|
conn.cursor()
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS beats (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
bpm INTEGER,
|
||||||
|
beatcount INTEGER,
|
||||||
|
beattime REAL,
|
||||||
|
createdAt TIMESTAMP DEFAULT (datetime('now','localtime'))
|
||||||
|
);
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# start a new WebSocket Application
|
||||||
|
# use "/" as the root, and the
|
||||||
|
# WebSocketHandler as our handler
|
||||||
|
application = tornado.web.Application([
|
||||||
|
(r"/", FrontendHandler, {"conn": conn}),
|
||||||
|
(r"/ws", WebSocketHandler, {"conn": conn}),
|
||||||
|
(r"/(.*).csv", ExportHandler, {"conn": conn}),
|
||||||
|
],debug=True)
|
||||||
|
|
||||||
|
application.listen(8888)
|
||||||
|
tornado.ioloop.IOLoop.instance().start()
|
64
heartbeat/ws.py
Normal file
64
heartbeat/ws.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import tornado.websocket
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
# This is our WebSocketHandler - it handles the messages
|
||||||
|
# from the tornado server
|
||||||
|
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
|
connections = set()
|
||||||
|
|
||||||
|
def initialize(self, conn):
|
||||||
|
"""
|
||||||
|
conn: sqlite3 connection
|
||||||
|
"""
|
||||||
|
self.conn = conn
|
||||||
|
|
||||||
|
# the client connected
|
||||||
|
def open(self):
|
||||||
|
self.connections.add(self)
|
||||||
|
print ("New client connected")
|
||||||
|
|
||||||
|
# the client sent the message
|
||||||
|
def on_message(self, message):
|
||||||
|
# {"rate":"77*", "count":"4", "time":"14.44"}
|
||||||
|
try:
|
||||||
|
beat = json.loads(message)
|
||||||
|
except Exception as e:
|
||||||
|
print("Probably no valid JSON")
|
||||||
|
[con.write_message(message) for con in self.connections]
|
||||||
|
return False
|
||||||
|
|
||||||
|
if beat['rate'].endswith('*'):
|
||||||
|
beat['rate'] = beat['rate'][:-1]
|
||||||
|
beat['rate'] = beat['rate'].strip()
|
||||||
|
if beat['count'].endswith('*'):
|
||||||
|
beat['count'] = beat['count'][:-1]
|
||||||
|
beat['count'] = beat['count'].strip()
|
||||||
|
if beat['time'].endswith('*'):
|
||||||
|
beat['time'] = beat['time'][:-1]
|
||||||
|
beat['time'] = beat['time'].strip()
|
||||||
|
|
||||||
|
c = self.conn.cursor()
|
||||||
|
c.execute("""
|
||||||
|
INSERT INTO beats (bpm,beatcount,beattime)
|
||||||
|
VALUES (:rate, :count, :time)
|
||||||
|
""", beat)
|
||||||
|
insertid = c.lastrowid
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
c.execute("SELECT createdAt FROM beats WHERE id = ?", (insertid,))
|
||||||
|
now = c.fetchone()[0]
|
||||||
|
|
||||||
|
beat['bpm'] = int(beat['rate'])
|
||||||
|
beat['timestamp'] = now
|
||||||
|
# actively close connection :-)
|
||||||
|
[con.write_message(json.dumps(beat)) for con in self.connections]
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# client disconnected
|
||||||
|
def on_close(self):
|
||||||
|
self.connections.remove(self)
|
||||||
|
print ("Client disconnected")
|
88
views/plot.html
Normal file
88
views/plot.html
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>WebSocket Test</title>
|
||||||
|
<style media="screen">
|
||||||
|
body{
|
||||||
|
font-family: Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
#graph{
|
||||||
|
height: calc(100vh - 5em);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script src="https://cdn.plot.ly/plotly-1.5.0.min.js"></script>
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
|
||||||
|
var wsUri = window.location.toString().replace('http','ws') + "ws";
|
||||||
|
console.log(wsUri)
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2 id="title">current bpm: <span id='value'>...</span></h2>
|
||||||
|
<div id='downloads'>
|
||||||
|
Claim data
|
||||||
|
<span>Last hour <a href='/1h.csv'>as CSV</a>.</span>
|
||||||
|
<span>Last day <a href='/24h.csv'>as CSV</a>.</span>
|
||||||
|
<span>Last week <a href='/week.csv'>as CSV</a>.</span>
|
||||||
|
</div>
|
||||||
|
<div id="graph">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<script type="text/javascript">
|
||||||
|
var valueEl = document.getElementById('value');
|
||||||
|
|
||||||
|
Plotly.d3.csv("/1h.csv", function(err, rows){
|
||||||
|
console.log(rows);
|
||||||
|
function unpack(rows, key) {
|
||||||
|
return rows.map(function(row) { return row[key]; });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var trace1 = {
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines",
|
||||||
|
name: 'BPM',
|
||||||
|
x: unpack(rows, 'timestamp'),
|
||||||
|
y: unpack(rows, 'bpm'),
|
||||||
|
line: {color: '#17BECF'}
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = [trace1];
|
||||||
|
|
||||||
|
var layout = {
|
||||||
|
title: 'Last hour',
|
||||||
|
};
|
||||||
|
|
||||||
|
Plotly.newPlot('graph', data, layout);
|
||||||
|
|
||||||
|
var wsUri = window.location.toString().replace('http','ws') + "ws";
|
||||||
|
|
||||||
|
var connectSocket = function() {
|
||||||
|
websocket = new WebSocket(wsUri);
|
||||||
|
websocket.onopen = function(evt) {
|
||||||
|
console.log("opened");
|
||||||
|
};
|
||||||
|
websocket.onclose = function(evt) {
|
||||||
|
console.log("closed", evt);
|
||||||
|
valueEl.innerHTML = '...';
|
||||||
|
setTimeout(connectSocket, 1000 * 10);
|
||||||
|
};
|
||||||
|
websocket.onmessage = function(evt) {
|
||||||
|
data = JSON.parse(evt.data);
|
||||||
|
var update = {
|
||||||
|
x: [[data['timestamp']]],
|
||||||
|
y: [[data['bpm']]]
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("update", update)
|
||||||
|
valueEl.innerHTML = data['bpm'];
|
||||||
|
|
||||||
|
Plotly.extendTraces('graph', update, [0])
|
||||||
|
};
|
||||||
|
websocket.onerror = function(evt) {
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
connectSocket();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
77
views/sockets.html
Normal file
77
views/sockets.html
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>WebSocket Test</title>
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
|
||||||
|
var wsUri = window.location.toString().replace('http','ws') + "ws";
|
||||||
|
console.log(wsUri)
|
||||||
|
var output;
|
||||||
|
|
||||||
|
function init()
|
||||||
|
{
|
||||||
|
output = document.getElementById("output");
|
||||||
|
testWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testWebSocket()
|
||||||
|
{
|
||||||
|
websocket = new WebSocket(wsUri);
|
||||||
|
websocket.onopen = function(evt) { onOpen(evt) };
|
||||||
|
websocket.onclose = function(evt) { onClose(evt) };
|
||||||
|
websocket.onmessage = function(evt) { onMessage(evt) };
|
||||||
|
websocket.onerror = function(evt) { onError(evt) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpen(evt)
|
||||||
|
{
|
||||||
|
writeToScreen("CONNECTED");
|
||||||
|
// doSend("WebSocket rocks");
|
||||||
|
document.getElementById('send').disabled = false;
|
||||||
|
document.getElementById('disable').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClose(evt)
|
||||||
|
{
|
||||||
|
writeToScreen("DISCONNECTED");
|
||||||
|
document.getElementById('send').disabled = true;
|
||||||
|
document.getElementById('disable').disabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMessage(evt)
|
||||||
|
{
|
||||||
|
// writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
|
||||||
|
// websocket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onError(evt)
|
||||||
|
{
|
||||||
|
// writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSend(message)
|
||||||
|
{
|
||||||
|
writeToScreen("SENT: " + message);
|
||||||
|
websocket.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeToScreen(message)
|
||||||
|
{
|
||||||
|
var pre = document.createElement("p");
|
||||||
|
pre.style.wordWrap = "break-word";
|
||||||
|
pre.innerHTML = message;
|
||||||
|
output.appendChild(pre);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("load", init, false);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2>WebSocket Test</h2>
|
||||||
|
|
||||||
|
{% for beat in beats %}
|
||||||
|
<li>{{beat['id'] }}: {{beat['bpm'] }} / {{beat['beatcount'] }} / {{beat['beattime'] }} / {{beat['createdAt'] }}</li>
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
<input type='text' id='msg' placeholder="message"> <button id='send' disabled onclick='doSend(document.getElementById("msg").value)'>Send message</button>
|
||||||
|
<button disabled onclick='websocket.close()' id='disable'>close connection</button>
|
||||||
|
<div id="output"></div>
|
Loading…
Reference in a new issue