Skip to content

Commit af84cd5

Browse files
authored
Merge pull request #119 from openxc/webdashtool
Webdashtool
2 parents e77a42c + d560a8f commit af84cd5

8 files changed

Lines changed: 200 additions & 214 deletions

File tree

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ include CONTRIBUTING.rst
33
include LICENSE
44
include openxc/generator/signals.cpp.footer
55
include openxc/generator/signals.cpp.header
6+
include openxc/tools/templates/dashboard.html

openxc/tools/dashboard.py

Lines changed: 48 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -1,211 +1,48 @@
1-
""" This module contains the methods for the ``openxc-dashboard`` command line
2-
program.
3-
4-
`main` is executed when ``openxc-dashboard`` is run, and all other callables in
5-
this module are internal only.
6-
"""
7-
8-
9-
10-
import argparse
11-
import curses
12-
import math
13-
from datetime import datetime
14-
from threading import Lock
15-
16-
from .common import device_options, configure_logging, select_device
17-
from openxc.vehicle import Vehicle
18-
from openxc.measurements import EventedMeasurement, Measurement
19-
20-
try:
21-
str
22-
except NameError:
23-
# Python 3
24-
str = str = str
25-
26-
27-
# timedelta.total_seconds() is only in 2.7, so we backport it here for 2.6
28-
def total_seconds(delta):
29-
return (delta.microseconds + (delta.seconds
30-
+ delta.days * 24 * 3600) * 10**6) / 10**6
31-
32-
33-
# Thanks, SO: http://stackoverflow.com/questions/1094841/reusable-library-to-get-human-readable-version-of-file-size
34-
def sizeof_fmt(num):
35-
for unit in ['bytes', 'KB', 'MB', 'GB', 'TB']:
36-
if num < 1024.0:
37-
return "%3.1f%s" % (num, unit)
38-
num /= 1024.0
39-
40-
41-
class DataPoint(object):
42-
AVERAGE_FREQUENCY_ALPHA = 0.1
43-
44-
def __init__(self, measurement_type):
45-
self.event = ''
46-
self.current_data = None
47-
self.events = {}
48-
self.messages_received = 0
49-
self.measurement_type = measurement_type
50-
self.min = None
51-
self.max = None
52-
self.last_update_time = None
53-
self.average_time_since_update = None
54-
55-
def update(self, measurement):
56-
self.messages_received += 1
57-
self.current_data = measurement
58-
59-
if self.last_update_time is not None:
60-
time_since_update = total_seconds(datetime.now() - self.last_update_time)
61-
if self.average_time_since_update is None:
62-
self.average_time_since_update = time_since_update
63-
else:
64-
self.average_time_since_update = ((self.AVERAGE_FREQUENCY_ALPHA *
65-
time_since_update) + (1 - self.AVERAGE_FREQUENCY_ALPHA) *
66-
self.average_time_since_update)
67-
self.last_update_time = datetime.now()
68-
69-
if getattr(self.current_data.value, 'unit', None) == self.current_data.unit:
70-
if self.min is None or self.current_data.value < self.min:
71-
self.min = self.current_data.value
72-
elif self.max is None or self.current_data.value > self.max:
73-
self.max = self.current_data.value
74-
75-
if isinstance(measurement, EventedMeasurement):
76-
if measurement.valid_state():
77-
self.events[measurement.value] = measurement.event
78-
79-
def percentage(self):
80-
# TODO man, this is getting really ugly to handle all of the different
81-
# types
82-
percent = None
83-
if hasattr(self.measurement_type, 'valid_range'):
84-
percent = self.current_data.percentage_within_range()
85-
elif (getattr(self, 'min', None) is not None and
86-
getattr(self, 'max', None) is not None) and self.min != self.max:
87-
percent = (((self.current_data.value - self.min) / float(self.max -
88-
self.min)) * 100).num
89-
return percent
90-
91-
def print_to_window(self, window, row, started_time):
92-
width = window.getmaxyx()[1]
93-
if self.current_data is not None:
94-
if len(self.events) == 0:
95-
value = str(self.current_data)
96-
else:
97-
result = ""
98-
for item, value in enumerate(self.measurement_type.states):
99-
# TODO missing keys here?
100-
result += "%s: %s " % (value, self.events.get(value, "?"))
101-
value = result
102-
103-
value_color = curses.color_pair(2)
104-
window.addstr(row, 0, "{:>22} %s".format(value), value_color)
105-
window.addstr(row, 23, self.current_data.name)
106-
107-
if width > 60:
108-
message_count_color = curses.color_pair(3)
109-
window.addstr(row, 50, "Msgs: " + str(self.messages_received),
110-
message_count_color)
111-
112-
if width > 75 and self.average_time_since_update > 0:
113-
window.addstr(row, 61, "Freq. (Hz): %d" %
114-
math.ceil((1 / self.average_time_since_update)))
115-
116-
117-
class Dashboard(object):
118-
def __init__(self, window, vehicle):
119-
self.window = window
120-
self.elements = {}
121-
self.scroll_position = 0
122-
self.screen_lock = Lock()
123-
vehicle.listen(Measurement, self.receive)
124-
125-
self.started_time = datetime.now()
126-
self.messages_received = 0
127-
128-
curses.use_default_colors()
129-
curses.init_pair(1, curses.COLOR_RED, -1)
130-
curses.init_pair(2, curses.COLOR_GREEN, -1)
131-
curses.init_pair(3, curses.COLOR_YELLOW, -1)
132-
133-
def receive(self, measurement, **kwargs):
134-
if self.messages_received == 0:
135-
self.started_time = datetime.now()
136-
self.messages_received += 1
137-
138-
if measurement.name not in self.elements:
139-
self.elements[measurement.name] = DataPoint(measurement.__class__)
140-
self.elements[measurement.name].update(measurement)
141-
self._redraw()
142-
143-
def _redraw(self):
144-
self.screen_lock.acquire()
145-
self.window.erase()
146-
max_rows = self.window.getmaxyx()[0] - 4
147-
for row, element in enumerate(sorted(list(self.elements.values()),
148-
key=lambda elt: elt.current_data.name)[self.scroll_position:]):
149-
if row > max_rows:
150-
break
151-
element.print_to_window(self.window, row, self.started_time)
152-
self.window.addstr(max_rows + 1, 0,
153-
"Message count: %d (%d corrupted)" % (self.messages_received,
154-
self.source.corrupted_messages), curses.A_REVERSE)
155-
self.window.addstr(max_rows + 2, 0,
156-
"Total received: %s" %
157-
sizeof_fmt(self.source.bytes_received),
158-
curses.A_REVERSE)
159-
self.window.addstr(max_rows + 3, 0, "Data Rate: %s" %
160-
sizeof_fmt(self.source.bytes_received /
161-
(total_seconds(datetime.now() - self.started_time)
162-
+ 0.1)),
163-
curses.A_REVERSE)
164-
self.window.refresh()
165-
self.screen_lock.release()
166-
167-
def scroll_down(self, lines):
168-
self.screen_lock.acquire()
169-
self.scroll_position = min(self.window.getmaxyx()[1],
170-
self.scroll_position + lines)
171-
self.screen_lock.release()
172-
173-
def scroll_up(self, lines):
174-
self.screen_lock.acquire()
175-
self.scroll_position = max(0, self.scroll_position - lines)
176-
self.screen_lock.release()
177-
178-
179-
def run_dashboard(window, source_class, source_kwargs):
180-
vehicle = Vehicle()
181-
dashboard = Dashboard(window, vehicle)
182-
dashboard.source = source_class(**source_kwargs)
183-
vehicle.add_source(dashboard.source)
184-
185-
window.scrollok(True)
186-
while True:
187-
c = window.getch()
188-
if c == curses.KEY_DOWN:
189-
dashboard.scroll_down(1)
190-
elif c == curses.KEY_UP:
191-
dashboard.scroll_up(1)
192-
elif c == curses.KEY_NPAGE:
193-
dashboard.scroll_down(25)
194-
elif c == curses.KEY_PPAGE:
195-
dashboard.scroll_up(25)
196-
197-
198-
199-
def parse_options():
200-
parser = argparse.ArgumentParser(
201-
description="View a real-time dashboard of all OpenXC measurements",
202-
parents=[device_options()])
203-
arguments = parser.parse_args()
204-
return arguments
205-
206-
207-
def main():
208-
configure_logging()
209-
arguments = parse_options()
210-
source_class, source_kwargs = select_device(arguments)
211-
curses.wrapper(run_dashboard, source_class, source_kwargs)
1+
""" This module contains the methods for the ``openxc-dashboard`` command line
2+
program.
3+
4+
`main` is executed when ``openxc-dashboard`` is run, and all other callables in
5+
this module are internal only.
6+
"""
7+
8+
import argparse
9+
import threading
10+
import logging
11+
from flask import Flask
12+
from flask import render_template
13+
from flask_socketio import SocketIO
14+
15+
from openxc.interface import UsbVehicleInterface
16+
17+
log = logging.getLogger('werkzeug')
18+
log.setLevel(logging.ERROR)
19+
app = Flask(__name__)
20+
socketio = SocketIO(app)
21+
22+
def vehicle_data_thread():
23+
vi = UsbVehicleInterface(callback=send_data)
24+
vi.start()
25+
26+
def send_data(data, **kwargs):
27+
socketio.emit('vehicle data', data, broadcast=True)
28+
29+
@app.route("/")
30+
def dashboard_static():
31+
try:
32+
vehicle_data_thread()
33+
except:
34+
pass
35+
return render_template('dashboard.html')
36+
37+
def parse_options():
38+
parser = argparse.ArgumentParser(
39+
description="View a real-time dashboard of all OpenXC measurements",
40+
parents=[device_options()])
41+
arguments = parser.parse_args()
42+
return arguments
43+
44+
45+
def main():
46+
socketio.start_background_task(vehicle_data_thread)
47+
print("View the dashboard at http://127.0.0.1:5000")
48+
app.run()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
caption {
2+
font-size: 1.5em;
3+
}
4+
5+
table, th, td {
6+
text-align: left;
7+
border-spacing: 8px 1px;
8+
}
9+
10+
.metric {
11+
text-align: right;
12+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
let dataPoints = {};
2+
3+
$(document).ready(function() {
4+
namespace = '';
5+
var socket = io(namespace);
6+
socket.on('vehicle data', function(msg, cb) {
7+
// console.log(msg);
8+
9+
if (!(msg.name in dataPoints)) {
10+
dataPoints[msg.name] = {
11+
current_data: undefined,
12+
events: {},
13+
messages_received: 0,
14+
measurement_type: undefined,
15+
min: undefined,
16+
max: undefined,
17+
last_update_time: undefined,
18+
average_time_since_update: undefined
19+
};
20+
}
21+
22+
updateDataPoint(dataPoints[msg.name], msg);
23+
updateDisplay(dataPoints[msg.name]);
24+
25+
if (cb)
26+
cb();
27+
});
28+
});
29+
30+
function addToDisplay(msgName) {
31+
// Insert new rows alphabetically
32+
var added = false;
33+
$('#log tr').each(function() {
34+
if (msgName < $(this).children('td:eq(0)').text()) {
35+
$('<tr/>', {
36+
id: msgName
37+
}).insertBefore($(this));
38+
added = true;
39+
return false;
40+
}
41+
});
42+
43+
if (!added) {
44+
$('<tr/>', {
45+
id: msgName
46+
}).appendTo('#log');
47+
}
48+
49+
$('<td/>', {
50+
id: msgName + '_label',
51+
text: msgName
52+
}).appendTo('#' + msgName);
53+
54+
$('<td/>', {
55+
id: msgName + '_value'
56+
}).appendTo('#' + msgName);
57+
58+
$('<td/>', {
59+
id: msgName + '_num',
60+
class: 'metric'
61+
}).appendTo('#' + msgName);
62+
63+
$('<td/>', {
64+
id: msgName + '_freq',
65+
class: 'metric'
66+
}).appendTo('#' + msgName);
67+
}
68+
69+
function updateDisplay(dataPoint) {
70+
msg = dataPoint.current_data
71+
72+
if (!($('#' + msg.name).length > 0)) {
73+
addToDisplay(msg.name);
74+
}
75+
76+
$('#' + msg.name + '_value').text(msg.value);
77+
$('#' + msg.name + '_num').text(dataPoint.messages_received);
78+
$('#' + msg.name + '_freq').text(Math.ceil(1 / dataPoint.average_time_since_update));
79+
}
80+
81+
function updateDataPoint(dataPoint, measurement) {
82+
dataPoint.messages_received++;
83+
dataPoint.current_data = measurement;
84+
let update_time = (new Date()).getTime() / 1000;
85+
86+
if (dataPoint.last_update_time !== undefined) {
87+
dataPoint.average_time_since_update =
88+
calculateAverageTimeSinceUpdate(update_time, dataPoint);
89+
}
90+
91+
dataPoint.last_update_time = update_time;
92+
93+
if ('event' in measurement) {
94+
dataPoint.events[measurement.value] = measurement.event;
95+
}
96+
}
97+
98+
function calculateAverageTimeSinceUpdate(updateTime, dataPoint) {
99+
let time_since_update = updateTime - dataPoint.last_update_time;
100+
101+
return (dataPoint.average_time_since_update === undefined)
102+
? time_since_update
103+
: (0.1 * time_since_update) + (0.9 * dataPoint.average_time_since_update);
104+
}

openxc/tools/static/js/jquery-3.4.1.slim.min.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openxc/tools/static/js/socket.io.slim.js

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)