Adding "Pronserver" - a network discoverable 3d printer service.

master
D1plo1d 2013-04-10 22:49:35 -04:00
parent eb0f808bf4
commit 5f1955901c
13 changed files with 555 additions and 3 deletions

View File

@ -49,7 +49,20 @@ The command box recognizes all pronsole commands, but has no tabcompletion.
If you want to load stl files, you need to install a slicing program such as Slic3r and add its path to the settings.
See the Slic3r readme for more details on integration.
# USING PRONSERVE
Pronserve runs a server for remote controlling your 3D printer over your network. To use pronserve you need:
* python (ideally 2.6.x or 2.7.x),
* pyserial (or python-serial on ubuntu/debian) and
* tornado
* D1plo1d's py-mdns fork (https://github.com/D1plo1d/py-mdns)
* pybonjour
* bonjour for windows (Windows ONLY)
When you're done setting up Printrun, you can start `pronserve.py` in the directory you unpacked it. Once the server starts you can verify it's working by going to http://localhost:8888 in your web browser.
# USING PRONSOLE

253
pronserve.py Executable file
View File

@ -0,0 +1,253 @@
#!/usr/bin/env python
import tornado.ioloop
import tornado.web
import tornado.websocket
from tornado import gen
import tornado.httpserver
import time
import base64
import logging
import logging.config
import cmd, sys
import glob, os, time, datetime
import sys, subprocess
import math, codecs
from math import sqrt
from gcoder import GCode
import printcore
from pprint import pprint
import pronsole
from server import basic_auth
import random
import textwrap
import SocketServer
import socket
import mdns
log = logging.getLogger("root")
# Authentication
# -------------------------------------------------
def authenticator(realm,handle,password):
"""
This method is a sample authenticator.
It treats authentication as successful
if the handle and passwords are the same.
It returns a tuple of handle and user name
"""
if handle == "admin" and password == "admin" :
return (handle,'Authorized User')
return None
def user_extractor(user_data):
"""
This method extracts the user handle from
the data structure returned by the authenticator
"""
return user_data[0]
def socket_auth(self):
user = self.get_argument("user", None)
password = self.get_argument("password", None)
return authenticator(None, user, password)
interceptor = basic_auth.interceptor
auth = basic_auth.authenticate('auth_realm', authenticator, user_extractor)
#@interceptor(auth)
# Routing
# -------------------------------------------------
class RootHandler(tornado.web.RequestHandler):
def get(self):
self.render("index.html")
class InspectHandler(tornado.web.RequestHandler):
def prepare(self):
auth(self, None)
def get(self):
self.render("inspect.html")
#class EchoWebSocketHandler(tornado.web.RequestHandler):
class ConstructSocketHandler(tornado.websocket.WebSocketHandler):
def _execute(self, transforms, *args, **kwargs):
if socket_auth(self):
super(ConstructSocketHandler, self)._execute(transforms, *args, **kwargs)
else:
self.stream.close();
def open(self):
pronserve.clients.add(self)
print "WebSocket opened. %i sockets currently open." % len(pronserve.clients)
def on_sensor_change(self):
self.write_message({'sensors': pronserve.sensors})
def on_pronsole_log(self, msg):
self.write_message({'log': {msg: msg, level: "debug"}})
def on_message(self, msg):
# TODO: the read bit of repl!
self.write_message("You said: " + msg)
def on_close(self):
pronserve.clients.remove(self)
print "WebSocket closed. %i sockets currently open." % len(pronserve.clients)
dir = os.path.dirname(__file__)
settings = dict(
template_path=os.path.join(dir, "server", "templates"),
static_path=os.path.join(dir, "server", "static"),
debug=True,
)
application = tornado.web.Application([
(r"/", RootHandler),
(r"/inspect", InspectHandler),
(r"/socket", ConstructSocketHandler)
], **settings)
# MDNS Server
# -------------------------------------------------
class MdnsServer(object):
def __init__(self):
self.servers = set()
# IPV4
# SocketServer.TCPServer((HOST, PORT), MyTCPHandler)
# sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# sock.bind(('0.0.0.0', 5353))
# sock.listen(5)
# self.sockets.add(sock)
# self.poll_rate = 0.01 # seconds
# IPV4
server = SocketServer.UDPServer(('0.0.0.0', 5353), MdnsHandler, False)
#server.allow_reuse_address = True;
server.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.socket.bind(('0.0.0.0', 5353))
# server.server_bind() # Manually bind, to support allow_reuse_address
server.server_activate() # (see above comment)
server.serve_forever()
#server.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.servers.add(server)
# IPV6
# sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
# sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# sock.bind(('0:0:0:0:0:0:0:0', 5353))
# sock.listen(5)
# self.sockets.add(sock)
class MdnsHandler(SocketServer.BaseRequestHandler):
def handle(self):
data = self.request[0].strip()
socket = self.request[1]
print "{} wrote:".format(self.client_address[0])
print data
socket.sendto(data.upper(), self.client_address)
# Pronserve: Server-specific functionality
# -------------------------------------------------
class Pronserve(pronsole.pronsole):
def __init__(self):
pronsole.pronsole.__init__(self)
self.settings.sensor_names = {'T': 'extruder', 'B': 'bed'}
self.stdout = sys.stdout
#self.init_Settings()
self.ioloop = tornado.ioloop.IOLoop.instance()
self.clients = set()
self.settings.sensor_poll_rate = 0.7 # seconds
self.sensors = {'extruder': -1, 'bed': -1}
self.load_default_rc()
#self.mdns = MdnsServer()
services = ({'type': '_construct._tcp', 'port': 8888, 'domain': "local."})
self.mdns = mdns.publisher().save_group({'name': 'pronserve', 'services': services })
self.recvcb("T:10 B:20")
def run_sensor_loop(self):
self.request_sensor_update()
next_timeout = time.time() + self.settings.sensor_poll_rate
gen.Task(self.ioloop.add_timeout(next_timeout, self.run_sensor_loop))
def request_sensor_update(self):
if self.p.online: self.p.send_now("M105")
self.fire("sensor_change")
def recvcb(self, l):
""" Parses a line of output from the printer via printcore """
l = l.rstrip()
print l
if "T:" in l:
self._receive_sensor_update(l)
if l!="ok" and not l.startswith("ok T") and not l.startswith("T:"):
self._receive_printer_error(l)
def _receive_sensor_update(self, l):
words = l.split(" ")
words.pop(0)
d = dict([ s.split(":") for s in words])
print list([ (key, value) for key, value in d.iteritems()])
for key, value in d.iteritems():
self.__update_item(key, value)
print "loop is over"
self.fire("sensor_change")
def __update_item(self, key, value):
sensor_name = self.settings.sensor_names[key]
self.sensors[sensor_name] = value
def fire(self, event_name, content=None):
for client in self.clients:
if content == None:
getattr(client, "on_" + event_name)()
else:
getattr(client, "on_" + event_name)(content)
def log(self, *msg):
msg = ''.join(str(i) for i in msg)
print msg
self.fire("pronsole_log", msg)
def write_prompt(self):
None
# Server Start Up
# -------------------------------------------------
print "Pronserve is starting..."
pronserve = Pronserve()
pronserve.do_connect("")
pronserve.run_sensor_loop()
if __name__ == "__main__":
application.listen(8888)
print "\n"+"-"*80
welcome = textwrap.dedent(u"""
+---+ \x1B[0;32mPronserve: Your printer just got a whole lot better.\x1B[0m
| \u2713 | Ready to print.
+---+ More details at http://localhost:8888/""")
sys.stdout.write(welcome)
print "\n\n" + "-"*80 + "\n"
try:
pronserve.ioloop.start()
except:
pronserve.p.disconnect()

View File

@ -258,6 +258,9 @@ class pronsole(cmd.Cmd):
def online(self):
self.log("printer is now online")
self.write_prompt()
def write_prompt(self):
sys.stdout.write(self.prompt)
sys.stdout.flush()
@ -817,8 +820,7 @@ class pronsole(cmd.Cmd):
tstring = l.rstrip()
if(tstring!="ok" and not tstring.startswith("ok T") and not tstring.startswith("T:") and not self.listing and not self.monitoring):
self.log(tstring)
sys.stdout.write(self.prompt)
sys.stdout.flush()
self.write_prompt()
for i in self.recvlisteners:
i(l)

0
server/__init__.py Normal file
View File

93
server/basic_auth.py Normal file
View File

@ -0,0 +1,93 @@
import tornado.ioloop
import tornado.web
import tornado.websocket
import base64
import logging
import logging.config
import tornado.httpserver
import tornado.ioloop
import tornado.web
log = logging.getLogger("root")
def authenticate(realm, authenticator,user_extractor) :
"""
This is a basic authentication interceptor which
protects the desired URIs and requires
authentication as per configuration
"""
def wrapper(self, transforms, *args, **kwargs):
def _request_basic_auth(self):
if self._headers_written:
raise Exception('headers have already been written')
# If this is a websocket accept parameter-based (user/password) auth:
if hasattr(self, 'stream'):
"""
self.stream.write(tornado.escape.utf8(
"HTTP/1.1 401 Unauthorized\r\n"+
"Date: Wed, 10 Apr 2013 02:09:52 GMT\r\n"+
"Content-Length: 0\r\n"+
"Content-Type: text/html; charset=UTF-8\r\n"+
"Www-Authenticate: Basic realm=\"auth_realm\"\r\n"+
"Server: TornadoServer/3.0.1\r\n\r\n"
))
self.stream.close()
"""
# If this is a restful request use the standard tornado methods:
else:
self.set_status(401)
self.set_header('WWW-Authenticate','Basic realm="%s"' % realm)
self._transforms = []
self.finish()
return False
request = self.request
format = ''
clazz = self.__class__
log.debug('intercepting for class : %s', clazz)
try:
auth_hdr = request.headers.get('Authorization')
if auth_hdr == None:
return _request_basic_auth(self)
if not auth_hdr.startswith('Basic '):
return _request_basic_auth(self)
auth_decoded = base64.decodestring(auth_hdr[6:])
username, password = auth_decoded.split(':', 2)
user_info = authenticator(realm, unicode(username), password)
if user_info :
self._user_info = user_info
self._current_user = user_extractor(user_info)
log.debug('authenticated user is : %s',
str(self._user_info))
else:
return _request_basic_auth(self)
except Exception, e:
return _request_basic_auth(self)
return True
return wrapper
def interceptor(func):
"""
This is a class decorator which is helpful in configuring
one or more interceptors which are able to intercept, inspect,
process and approve or reject further processing of the request
"""
def classwrapper(cls):
def wrapper(old):
def inner(self, transforms, *args, **kwargs):
log.debug('Invoking wrapper %s',func)
ret = func(self,transforms,*args,**kwargs)
if ret :
return old(self,transforms,*args,**kwargs)
else :
return ret
return inner
cls._execute = wrapper(cls._execute)
return cls
return classwrapper
print "moo"

BIN
server/static/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,34 @@
html, body
{
margin: 0px;
padding: 0px;
height: 100%;
}
body
{
background: url("/static/img/background.jpg");
background-color: black;
background-repeat: no-repeat;
background-position: 50% 50%;
background-size: auto 100%;
}
.lead-box
{
position: absolute;
text-align: right;
top: 80%;
margin-top: -40px;
padding: 0px;
color: white;
padding-left: 100px;
padding-right: 20px;
background: rgba(0, 0, 0, 0.6);
}
.lead-box a, .lead-box a:hover
{
color: #3198EC;
font-weight: bold;
}

View File

@ -0,0 +1,36 @@
.sensors
{
margin-bottom: 20px;
}
.sensors>*
{
margin-left: 40px;
font-weight: bold;
font-size: 120%;
}
.sensors .val
{
display: inline-block;
width: 50px;
text-align: right;
}
.sensors .val, .sensors .deg
{
font-weight: normal;
}
.console
{
height: 300px;
overflow-y: scroll;
}
.console pre
{
border: 0px;
margin: 0px;
padding: 0px;
}

BIN
server/static/img/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

View File

@ -0,0 +1,56 @@
console.log("w00t!");
var $console;
var connect = function() {
// Let us open a web socket
var url = "ws://localhost:8888/socket?user=admin&password=admin";
console.log(url);
var ws = new WebSocket(url);
$(function () {
$consoleWrapper = $(".console");
$console = $(".console pre");
$console.html("Connecting...")
onConnect(ws)
});
};
var onConnect = function(ws) {
ws.onopen = function()
{
$console.append("\nConnected.");
// Web Socket is connected, send data using send()
};
ws.onmessage = function (evt)
{
msg = JSON.parse(evt.data)
if(msg.sensors != undefined)
{
var sensorNames = ["bed", "extruder"];
for (var i = 0; i < sensorNames.length; i++)
{
var name = sensorNames[i];
var val = parseFloat(msg.sensors[name]);
$("."+name+" .val").html(val.format(2));
}
}
$console.append("\n"+evt.data);
$consoleWrapper.scrollTop($console.innerHeight());
};
ws.onclose = function()
{
// websocket is closed.
$console.append("\nConnection closed.");
};
};
if ("WebSocket" in window)
{
connect();
}
else
{
// The browser doesn't support WebSocket
alert("Error: WebSocket NOT supported by your Browser!");
}

View File

@ -0,0 +1,23 @@
<!doctype html>
<html>
<head>
<title>Pronserve</title>
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
<link href="/static/css/index.css" rel="stylesheet">
</head>
<body>
<div class="lead-box">
<h1>
Your printer just got a whole lot better.
</h1>
<p class="lead">
Pronserve is ready to print. Why not try it out with
<a href="https://github.com/D1plo1d/ctrlpanel">Ctrl Panel</a>?
</p>
</div>
</body>
</html>

View File

@ -0,0 +1,42 @@
<!doctype html>
<html>
<head>
<title>Pronserve Inspector</title>
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
<link href="/static/css/inspect.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="row">
<div class="span12">
<h1>
Pronserve Inspector
</h1>
<div class="sensors">
<div class="extruder pull-right">
Extruder: <span class="val">xx.xx</span><span class="deg">&deg;C</span>
</div>
<div class="bed pull-right">
Bed: <span class="val"/>xx.xx</span><span class="deg">&deg;C</span>
</div>
<div class="clearfix"></div>
</div>
<div class="well console">
<pre>
Connecting...
</pre>
</div>
</div>
</div>
</div>
<script src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sugar/1.3.9/sugar.min.js"></script>
<script src="/static/js/inspect.js"></script>
</body>
</html>