Skip to content

Commit 55c2478

Browse files
authored
Merge pull request #122 from openxc/next
1.1.0 release
2 parents 3b92a35 + 1720a53 commit 55c2478

16 files changed

Lines changed: 267 additions & 240 deletions

.travis.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,10 @@ script: python setup.py test
1212
after_success:
1313
- coverage run --source=openxc setup.py test
1414
- coveralls
15+
deploy:
16+
provider: pypi
17+
server: https://pypi.org/legacy/
18+
user: "__token__"
19+
password: $PYPI_PASSWORD
20+
on:
21+
branch: master

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
OpenXC Python Library Changelog
22
===============================
33

4+
v1.1.0
5+
----------
6+
* Feature: Generate firmware now checks for duplicate entries in json and for improperly used keys and unrecognised attributes.
7+
* Feature: openxc-dashboad has been updated from a command line tool to a web server and webpage.
8+
* Fix: Requiring windows curses on non windows system
9+
* Fix: Various Python 3 bugs
10+
411
v1.0.0
512
----------
613
* Remove python 2.7 support

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

README.rst

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
OpenXC for Python
33
===============================================
44

5-
.. image:: /docs/_static/logo.png
5+
.. image:: https://github.com/openxc/openxc-python/raw/master/docs/_static/logo.png
66

7-
:Version: 1.0.0
7+
:Version: 1.1.0
88
:Web: http://openxcplatform.com
99
:Download: http://pypi.python.org/pypi/openxc/
1010
:Documentation: http://python.openxcplatform.com
@@ -31,15 +31,16 @@ number of command-line tools for connecting to the CAN translator and
3131
manipulating previously recorded vehicle data.
3232

3333
To package run "setup.py sdist bdist_wheel"
34-
to push to pypi run "python -m twine upload dist/*"
34+
to push to pypi run "python -m twine upload dist/\*"
3535
Version files:
36-
CHANGELOG.rst
37-
README.rst
38-
openxc/version.py
39-
docs/index.rst
36+
37+
- CHANGELOG.rst
38+
- README.rst
39+
- openxc/version.py
40+
- docs/index.rst
4041

4142
License
42-
=======
43+
========
4344

4445
Copyright (c) 2012-2017 Ford Motor Company
4546

docs/index.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
OpenXC for Python
33
===================
44

5-
.. image:: /_static/logo.png
5+
.. image:: https://github.com/openxc/openxc-python/raw/master/docs/_static/logo.png
66

7-
:Version: 1.0.0
7+
:Version: 1.1.0
88
:Web: http://openxcplatform.com
99
:Download: http://pypi.python.org/pypi/openxc/
1010
:Documentation: http://python.openxcplatform.com

openxc/generator/structures.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import operator
22
import math
33
from collections import defaultdict
4+
from openxc.utils import fatal_error
45

56
import logging
67

@@ -102,6 +103,14 @@ def id(self, value):
102103
def merge_message(self, data):
103104
self.bus_name = self.bus_name or data.get('bus', None)
104105

106+
message_attributes = dir(self)
107+
message_attributes = [a.replace('bus_name', 'bus') for a in message_attributes]
108+
data_attributes = list(data.keys())
109+
extra_attributes = set(data_attributes) - set(message_attributes)
110+
111+
if extra_attributes:
112+
fatal_error('ERROR: Message %s has unrecognized attributes: %s' % (data.get('id'), ', '.join(extra_attributes)))
113+
105114
if getattr(self, 'message_set'):
106115
self.bus = self.message_set.lookup_bus(name=self.bus_name)
107116
if not self.bus.valid():
@@ -353,6 +362,14 @@ def merge_signal(self, data):
353362
"%s is deprecated and has no effect " % self.generic_name +
354363
" - see the replacement, max_frequency")
355364

365+
signal_attributes = dir(self)
366+
data_attributes = list(data.keys())
367+
extra_attributes = set(data_attributes) - set(signal_attributes)
368+
369+
if extra_attributes:
370+
fatal_error('ERROR: Signal %s in %s has unrecognized attributes: %s' % (self.name, self.message.name, ', '.join(extra_attributes)))
371+
372+
356373
@property
357374
def decoder(self):
358375
decoder = getattr(self, '_decoder', None)

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+
}

0 commit comments

Comments
 (0)