Adding "Pronserver" - a network discoverable 3d printer service.
parent
eb0f808bf4
commit
5f1955901c
15
README.md
15
README.md
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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,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"
|
Binary file not shown.
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 444 KiB |
|
@ -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!");
|
||||
}
|
|
@ -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>
|
|
@ -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">°C</span>
|
||||
</div>
|
||||
<div class="bed pull-right">
|
||||
Bed: <span class="val"/>xx.xx</span><span class="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>
|
Loading…
Reference in New Issue