Skip to content

Commit 503b956

Browse files
committed
Initial commit
0 parents  commit 503b956

File tree

11 files changed

+681
-0
lines changed

11 files changed

+681
-0
lines changed

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
*.py[cod]
2+
dist
3+
build
4+
MANIFEST
5+
__pycache__
6+
*.egg-info
7+
8+
version.py
9+
10+
.project
11+
.pydevproject

LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Copyright © 2015 Dustin Spicuzza
2+
Copyright © 2015 Leon Tan
3+
Copyright © 2015 Contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.
22+

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
recursive-include example *
2+
recursive-include pynetworktables2js/js *

README.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
pynetworktables2js
2+
==================
3+
4+
A library that forwards NetworkTables key/values over a Websocket, so that
5+
you can easily write your dashboard in HTML5 + JavaScript.
6+
7+
This library does not provide a full dashboard solution, but is intended to
8+
provide the necessary plumbing for one to create one with only knowledge
9+
of HTML/Javascript. Because the communication layer uses NetworkTables, you
10+
can connect to all FRC languages (C++, Java, LabVIEW, Python).
11+
12+
Installation
13+
============
14+
15+
Make sure to install python 2 or 3 on your computer, and on Windows you can
16+
execute::
17+
18+
py -m pip install pynetworktables2js
19+
20+
On Linux/OSX you can execute::
21+
22+
pip install pynetworktables2js
23+
24+
25+
Usage
26+
=====
27+
28+
Go to the 'example' directory distributed with pynetworktables2js, and run::
29+
30+
python server.py --robot 127.0.0.1
31+
32+
If you navigate your browser (I recommend Chrome) to http://127.0.0.1:8888, all
33+
of the current NetworkTables values will be shown as they change.
34+
35+
One way of testing this out is use the TableViewer application, and start it in
36+
server mode.
37+
38+
Feel free to copy the example directory to create your own customized
39+
dashboard. Just add your custom files to the www directory.
40+
41+
Authors
42+
=======
43+
44+
Leon Tan of FRC Team 1418 did the initial research/work to get this working,
45+
and created an initial working prototype for Team 1418's 2015 Dashboard.
46+
47+
Dustin Spicuzza cleaned stuff up, rewrote things, added more functionality,
48+
and packaged it so other teams could use it.

example/server.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env python
2+
'''
3+
This is an example server application that you can use to connect your
4+
HTML/Javascript dashboard code to your robot via NetworkTables.
5+
6+
Run this application with python, then you can open your browser to
7+
http://localhost:8888/ to view the index.html page.
8+
'''
9+
10+
from os.path import abspath, dirname, join
11+
from optparse import OptionParser
12+
13+
import tornado.web
14+
from tornado.ioloop import IOLoop
15+
16+
from networktables import NetworkTable
17+
from pynetworktables2js import get_handlers, NonCachingStaticFileHandler
18+
19+
import logging
20+
logger = logging.getLogger('dashboard')
21+
22+
log_datefmt = "%H:%M:%S"
23+
log_format = "%(asctime)s:%(msecs)03d %(levelname)-8s: %(name)-20s: %(message)s"
24+
25+
def init_networktables(ipaddr):
26+
27+
logger.info("Connecting to networktables at %s" % ipaddr)
28+
NetworkTable.setIPAddress(ipaddr)
29+
NetworkTable.setClientMode()
30+
NetworkTable.initialize()
31+
logger.info("Networktables Initialized")
32+
33+
34+
if __name__ == '__main__':
35+
36+
# Setup options here
37+
parser = OptionParser()
38+
39+
parser.add_option('-p', '--port', default=8888,
40+
help='Port to run web server on')
41+
42+
parser.add_option('-v', '--verbose', default=False, action='store_true',
43+
help='Enable verbose logging')
44+
45+
parser.add_option('--robot', default='127.0.0.1',
46+
help="Robot's IP address")
47+
48+
options, args = parser.parse_args()
49+
50+
# Setup logging
51+
logging.basicConfig(datefmt=log_datefmt,
52+
format=log_format,
53+
level=logging.DEBUG if options.verbose else logging.INFO)
54+
55+
# Setup NetworkTables
56+
init_networktables(options.robot)
57+
58+
# setup tornado application with static handler + networktables support
59+
www_dir = abspath(join(dirname(__file__), 'www'))
60+
61+
app = tornado.web.Application(
62+
get_handlers() + [
63+
(r"/()", NonCachingStaticFileHandler, {"path": join(www_dir, 'index.html')}),
64+
(r"/(.*)", NonCachingStaticFileHandler, {"path": www_dir})
65+
]
66+
)
67+
68+
# Start the app
69+
logger.info("Listening on http://localhost:%s/" % options.port)
70+
71+
app.listen(options.port)
72+
IOLoop.current().start()

example/www/index.html

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
</head>
6+
<body>
7+
8+
<!-- This starts the NetworkTables websocket, it can be accessed from multiple
9+
pages simultaneously -->
10+
<script src="/networktables/networktables.js"></script>
11+
12+
<!-- Obviously, you will want to copy this file locally in a real
13+
dashboard, as the Driver Station won't have internet access -->
14+
<script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
15+
16+
<div>
17+
NetworkTables websocket: <span id="connectstate">Unknown state</span><br/>
18+
Robot: <span id="robotstate">Unknown state</span>
19+
</div>
20+
<hr/>
21+
22+
<table id="nt" border=1>
23+
<tbody></tbody>
24+
</table>
25+
26+
27+
<script type="text/javascript">
28+
"use strict";
29+
30+
$(document).ready(function(){
31+
32+
// sets a function that will be called when the websocket connects/disconnects
33+
NetworkTables.addWsConnectionListener(onNetworkTablesConnection, true);
34+
35+
// sets a function that will be called when the robot connects/disconnects
36+
NetworkTables.addRobotConnectionListener(onRobotConnection, true);
37+
38+
// sets a function that will be called when any NetworkTables key/value changes
39+
NetworkTables.addGlobalListener(onValueChanged, true);
40+
});
41+
42+
43+
function onRobotConnection(connected) {
44+
$('#robotstate').text(connected ? "Connected!" : "Disconnected");
45+
}
46+
47+
function onNetworkTablesConnection(connected) {
48+
49+
if (connected) {
50+
$("#connectstate").text("Connected!");
51+
52+
// clear the table
53+
$("#nt tbody > tr").remove();
54+
55+
} else {
56+
$("#connectstate").text("Disconnected!");
57+
}
58+
}
59+
60+
function onValueChanged(key, value, isNew) {
61+
62+
// key thing here: we're using the various NetworkTable keys as
63+
// the id of the elements that we're appending, for simplicity. However,
64+
// the key names aren't always valid HTML identifiers, so we use
65+
// the NetworkTables.keyToId() function to convert them appropriately
66+
67+
if (isNew) {
68+
var tr = $('<tr></tr>').appendTo($('#nt > tbody:last'));
69+
$('<td></td>').text(key).appendTo(tr);
70+
$('<td></td>').attr('id', NetworkTables.keyToId(key))
71+
.text(value)
72+
.appendTo(tr);
73+
} else {
74+
75+
// similarly, use keySelector to convert the key to a valid jQuery
76+
// selector. This should work for class names also, not just for ids
77+
$('#' + NetworkTables.keySelector(key)).text(value);
78+
}
79+
}
80+
81+
</script>
82+
83+
</body>
84+
</html>

pynetworktables2js/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
from .handlers import NetworkTablesWebSocket, NonCachingStaticFileHandler, get_handlers

pynetworktables2js/handlers.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
2+
from os.path import abspath, dirname, join
3+
4+
try:
5+
from ujson import loads
6+
except ImportError:
7+
from json import loads
8+
9+
10+
from tornado.ioloop import IOLoop
11+
from tornado.web import StaticFileHandler
12+
from tornado.websocket import WebSocketHandler, WebSocketClosedError
13+
14+
15+
from networktables import NetworkTable
16+
17+
import logging
18+
logger = logging.getLogger('net2js')
19+
20+
__all__ = ['get_handlers', 'NetworkTablesWebSocket', 'NonCachingStaticFileHandler']
21+
22+
class NetworkTablesWebSocket(WebSocketHandler):
23+
'''
24+
A tornado web handler that forwards values between NetworkTables
25+
and a webpage via a websocket
26+
'''
27+
28+
def open(self):
29+
logger.info("NetworkTables websocket opened")
30+
31+
self.ioloop = IOLoop.current()
32+
self.nt = NetworkTable.getGlobalTable()
33+
NetworkTable.addGlobalListener(self.on_nt_change, immediateNotify=True)
34+
self.nt.addConnectionListener(self, immediateNotify=True)
35+
36+
def on_message(self, message):
37+
# called when message is received from the dashboard
38+
data = loads(message)
39+
self.nt.putValue(data['k'], data['v'])
40+
41+
def on_close(self):
42+
logger.info("NetworkTables websocket closed")
43+
NetworkTable.removeGlobalListener(self.on_nt_change)
44+
45+
def send_to_dashboard(self, msg):
46+
try:
47+
self.write_message(msg, False)
48+
except WebSocketClosedError:
49+
logger.warn("websocket closed when sending message")
50+
51+
#
52+
# NetworkTables API
53+
#
54+
# These functions cannot directly access the websocket, as they are
55+
# called from another thread
56+
57+
def on_nt_change(self, key, value, isNew):
58+
self.ioloop.add_callback(self.send_to_dashboard,
59+
{'k': key, 'v': value, 'n': isNew})
60+
61+
def connected(self, table):
62+
self.ioloop.add_callback(self.send_to_dashboard,
63+
{'r': True})
64+
65+
def disconnected(self, table):
66+
self.ioloop.add_callback(self.send_to_dashboard,
67+
{'r': False})
68+
69+
70+
class NonCachingStaticFileHandler(StaticFileHandler):
71+
'''
72+
This static file handler disables caching, to allow for easy
73+
development of your Dashboard
74+
'''
75+
76+
# This is broken in tornado, disable it
77+
def check_etag_header(self):
78+
return False
79+
80+
def set_extra_headers(self, path):
81+
# Disable caching
82+
self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
83+
84+
85+
def get_handlers():
86+
'''
87+
Returns a list that can be concatenated to the list of handlers
88+
passed to the ``tornado.web.Application`` object. This list contains
89+
handlers for the NetworkTables websocket and the necessary javascript
90+
to use it.
91+
92+
Example usage::
93+
94+
import pynetworktables2js
95+
import tornado.web
96+
97+
...
98+
99+
app = tornado.web.Application(
100+
pynetworktables2js.get_handlers() + [
101+
# tornado handlers here
102+
])
103+
'''
104+
105+
js_path_opts = {'path': abspath(join(dirname(__file__), 'js', 'networktables.js'))}
106+
107+
return [
108+
('/networktables/ws', NetworkTablesWebSocket),
109+
('/networktables/networktables.js()', NonCachingStaticFileHandler, js_path_opts),
110+
]
111+

0 commit comments

Comments
 (0)