diff --git a/README.md b/README.md index 0fcbd49..1912a53 100644 --- a/README.md +++ b/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 diff --git a/pronserve.py b/pronserve.py new file mode 100755 index 0000000..011f070 --- /dev/null +++ b/pronserve.py @@ -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() diff --git a/pronsole.py b/pronsole.py index b020661..3d78830 100755 --- a/pronsole.py +++ b/pronsole.py @@ -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) diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/basic_auth.py b/server/basic_auth.py new file mode 100644 index 0000000..e54983a --- /dev/null +++ b/server/basic_auth.py @@ -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" \ No newline at end of file diff --git a/server/static/.DS_Store b/server/static/.DS_Store new file mode 100644 index 0000000..f5abd5e Binary files /dev/null and b/server/static/.DS_Store differ diff --git a/server/static/css/index.css b/server/static/css/index.css new file mode 100644 index 0000000..153f467 --- /dev/null +++ b/server/static/css/index.css @@ -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; +} diff --git a/server/static/css/inspect.css b/server/static/css/inspect.css new file mode 100644 index 0000000..2ab8d54 --- /dev/null +++ b/server/static/css/inspect.css @@ -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; +} \ No newline at end of file diff --git a/server/static/img/.DS_Store b/server/static/img/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/server/static/img/.DS_Store differ diff --git a/server/static/img/background.jpg b/server/static/img/background.jpg new file mode 100644 index 0000000..c462050 Binary files /dev/null and b/server/static/img/background.jpg differ diff --git a/server/static/js/inspect.js b/server/static/js/inspect.js new file mode 100644 index 0000000..8b126a7 --- /dev/null +++ b/server/static/js/inspect.js @@ -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!"); +} \ No newline at end of file diff --git a/server/templates/index.html b/server/templates/index.html new file mode 100644 index 0000000..65b5752 --- /dev/null +++ b/server/templates/index.html @@ -0,0 +1,23 @@ + + + + Pronserve + + + + + + +
+

+ Your printer just got a whole lot better. +

+

+ Pronserve is ready to print. Why not try it out with + Ctrl Panel? +

+
+ + + + \ No newline at end of file diff --git a/server/templates/inspect.html b/server/templates/inspect.html new file mode 100644 index 0000000..d93baf3 --- /dev/null +++ b/server/templates/inspect.html @@ -0,0 +1,42 @@ + + + + Pronserve Inspector + + + + + + +
+
+
+

+ Pronserve Inspector +

+
+
+ Extruder: xx.xx°C +
+
+ Bed: xx.xx°C +
+
+
+
+
+            Connecting...
+          
+
+
+
+
+ + + + + + + + + \ No newline at end of file