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