diff --git a/.gitignore b/.gitignore index e8c9a13..6ade96c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .pronsolerc *.swp *.bak +uploads diff --git a/README.md b/README.md index 9a1837d..1912a53 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ Printrun consists of printcore, pronsole and pronterface, and a small collection * printcore.py is a library that makes writing reprap hosts easy * pronsole.py is an interactive command-line host software with tabcompletion goodness * pronterface.py is a graphical host software with the same functionality as pronsole - * webinterface.py is a browser-usable remote control function for Pronterface # GETTING PRINTRUN @@ -51,23 +50,19 @@ 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 the browser interface -To run the web interface, install Cherrypy and run Pronterface as described above. -The www server will start on the port/address you have chosen. +# USING PRONSERVE -## Webinterface Dependencies +Pronserve runs a server for remote controlling your 3D printer over your network. To use pronserve you need: -Cherrypy is required for the web interface. Download and install it by opening a -command prompt in its directory and running python setup.py install. + * 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) -## Webinterface Configuration - * The Web interface port / ip is configurable in http.config - * The Default User / Password can be set in auth.config - -## Webinterface Styling - * css/style.css can be modified to change the style of the Web Interface. - +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/php/parser.php b/php/parser.php deleted file mode 100644 index 86ac16f..0000000 --- a/php/parser.php +++ /dev/null @@ -1,35 +0,0 @@ -state . "
"; - echo "Hotend: " . round($xml->hotend, 0) . "°c
"; - echo "Bed: " . round($xml->bed, 0) . "°c
"; - if ($xml->progress != "NA") - { - echo "Progress: " . $xml->progress . "%"; - } -} -catch(Exception $e) -{ - echo "ERROR:\n" . $e->getMessage(). " (severity " . $e->getCode() . ")"; -} -?> \ No newline at end of file diff --git a/printrun/webinterface.py b/printrun/webinterface.py deleted file mode 100644 index bb9690b..0000000 --- a/printrun/webinterface.py +++ /dev/null @@ -1,397 +0,0 @@ -#!/usr/bin/python -# This file is part of the Printrun suite. -# -# Printrun is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Printrun is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Printrun. If not, see . - -import pronterface -import cherrypy, re, ConfigParser, threading, sys -import os.path - -from printrun.printrun_utils import configfile, imagefile, sharedfile - -users = {} - -def PrintHeader(): - return '\n\nPronterface-Web\n\n\n\n\n' - -def PrintMenu(): - return '' - -def PrintFooter(): - return "" - -def ReloadPage(action): - return ""+action+"" - -def TReloadPage(action): - return action - -def clear_text(mypass): - return mypass - -gPronterPtr = 0 -gWeblog = "" -gLogRefresh =5 -class SettingsPage(object): - def __init__(self): - self.name = "
Pronterface Settings
" - - def index(self): - pageText = PrintHeader()+self.name+PrintMenu() - pageText = pageText+"
\n" - pageText = pageText+"\n \n" - pageText = pageText+" \n \n" - pageText = pageText+" \n \n" - pageText = pageText+" \n \n" - pageText = pageText+" \n \n" - pageText = pageText+" \n " - pageText = pageText+PrintFooter() - return pageText - index.exposed = True - -class LogPage(object): - def __init__(self): - self.name = "
Pronterface Console
" - - def index(self): - pageText = "" - pageText+="
" - pageText+=gPronterPtr.status.GetStatusText() - pageText+="
" - pageText = pageText+"
"+gWeblog+"
" - pageText = pageText+"" - return pageText - index.exposed = True - -class ConsolePage(object): - def __init__(self): - self.name = "
Pronterface Settings
" - - def index(self): - pageText = PrintHeader()+self.name+PrintMenu() - pageText+="
" - pageText+=PrintFooter() - return pageText - index.exposed = True - -class ConnectButton(object): - def index(self): - #handle connect push, then reload page - gPronterPtr.connect(0) - return ReloadPage("Connect...") - index.exposed = True - index._cp_config = {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'My Print Server', - 'tools.basic_auth.users': users, - 'tools.basic_auth.encrypt': clear_text} - -class DisconnectButton(object): - def index(self): - #handle connect push, then reload page - gPronterPtr.disconnect(0) - return ReloadPage("Disconnect...") - index.exposed = True - index._cp_config = {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'My Print Server', - 'tools.basic_auth.users': users, - 'tools.basic_auth.encrypt': clear_text} - -class ResetButton(object): - def index(self): - #handle connect push, then reload page - gPronterPtr.reset(0) - return ReloadPage("Reset...") - index.exposed = True - index._cp_config = {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'My Print Server', - 'tools.basic_auth.users': users, - 'tools.basic_auth.encrypt': clear_text} - -class PrintButton(object): - def index(self): - #handle connect push, then reload page - gPronterPtr.printfile(0) - return ReloadPage("Print...") - index.exposed = True - index._cp_config = {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'My Print Server', - 'tools.basic_auth.users': users, - 'tools.basic_auth.encrypt': clear_text} - -class PauseButton(object): - def index(self): - #handle connect push, then reload page - gPronterPtr.pause(0) - return ReloadPage("Pause...") - index.exposed = True - index._cp_config = {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'My Print Server', - 'tools.basic_auth.users': users, - 'tools.basic_auth.encrypt': clear_text} - -class MoveButton(object): - def axis(self, *args): - if not args: - raise cherrypy.HTTPError(400, "No Move Command Provided!") - margs = list(args) - axis = margs.pop(0) - if(margs and axis == "x"): - distance = margs.pop(0) - gPronterPtr.onecmd('move X %s' % distance) - return ReloadPage("Moving X Axis " + str(distance)) - if(margs and axis == "y"): - distance = margs.pop(0) - gPronterPtr.onecmd('move Y %s' % distance) - return ReloadPage("Moving Y Axis " + str(distance)) - if(margs and axis == "z"): - distance = margs.pop(0) - gPronterPtr.onecmd('move Z %s' % distance) - return ReloadPage("Moving Z Axis " + str(distance)) - raise cherrypy.HTTPError(400, "Unmached Move Command!") - axis.exposed = True - axis._cp_config = {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'My Print Server', - 'tools.basic_auth.users': users, - 'tools.basic_auth.encrypt': clear_text} - -class CustomButton(object): - def button(self, *args): - if not args: - raise cherrypy.HTTPError(400, "No Custom Command Provided!") - margs = list(args) - command = margs.pop(0) - if(command): - gPronterPtr.onecmd(command) - return ReloadPage(str(command)) - button.exposed = True - button._cp_config = {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'My Print Server', - 'tools.basic_auth.users': users, - 'tools.basic_auth.encrypt': clear_text} - -class HomeButton(object): - def axis(self, *args): - if not args: - raise cherrypy.HTTPError(400, "No Axis Provided!") - margs = list(args) - taxis = margs.pop(0) - if(taxis == "x"): - gPronterPtr.onecmd('home X') - return ReloadPage("Home X") - if(taxis == "y"): - gPronterPtr.onecmd('home Y') - return ReloadPage("Home Y") - if(taxis == "z"): - gPronterPtr.onecmd('home Z') - return ReloadPage("Home Z") - if(taxis == "all"): - gPronterPtr.onecmd('home') - return ReloadPage("Home All") - - axis.exposed = True - axis._cp_config = {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'My Print Server', - 'tools.basic_auth.users': users, - 'tools.basic_auth.encrypt': clear_text} - -class XMLstatus(object): - def index(self): - #handle connect push, then reload page - txt='\n\n' - state = "Offline" - if gPronterPtr.statuscheck or gPronterPtr.p.online: - state = "Idle" - if gPronterPtr.sdprinting: - state = "SDPrinting" - if gPronterPtr.p.printing: - state = "Printing" - if gPronterPtr.paused: - state = "Paused" - - txt = txt+''+state+'\n' - txt = txt+''+str(gPronterPtr.filename)+'\n' - txt = txt+''+str(gPronterPtr.status.GetStatusText())+'\n' - try: - temp = str(float(filter(lambda x:x.startswith("T:"), gPronterPtr.tempreport.split())[0].split(":")[1])) - txt = txt+''+temp+'\n' - except: - txt = txt+'NA\n' - pass - try: - temp = str(float(filter(lambda x:x.startswith("B:"), gPronterPtr.tempreport.split())[0].split(":")[1])) - txt = txt+''+temp+'\n' - except: - txt = txt+'NA\n' - pass - if gPronterPtr.sdprinting: - fractioncomplete = float(gPronterPtr.percentdone/100.0) - txt+= _("%04.2f") % (gPronterPtr.percentdone,) - txt+="\n" - elif gPronterPtr.p.printing: - fractioncomplete = float(gPronterPtr.p.queueindex)/len(gPronterPtr.p.mainqueue) - txt+= _("%04.2f") % (100*float(gPronterPtr.p.queueindex)/len(gPronterPtr.p.mainqueue),) - txt+="\n" - else: - txt+="NA\n" - txt+='' - return txt - index.exposed = True - -class WebInterface(object): - - def __init__(self, pface): - if (sys.version_info[1] > 6): - # 'allow_no_value' wasn't added until 2.7 - config = ConfigParser.SafeConfigParser(allow_no_value = True) - else: - config = ConfigParser.SafeConfigParser() - config.read(configfile(pface.web_auth_config or 'auth.config')) - users[config.get("user", "user")] = config.get("user", "pass") - self.pface = pface - global gPronterPtr - global gWeblog - self.name = "
Pronterface Web-Interface
" - gWeblog = "" - gPronterPtr = self.pface - - settings = SettingsPage() - logpage = LogPage() - console = ConsolePage() - - #actions - connect = ConnectButton() - disconnect = DisconnectButton() - reset = ResetButton() - printbutton = PrintButton() - pausebutton = PrintButton() - status = XMLstatus() - home = HomeButton() - move = MoveButton() - custom =CustomButton() - - def index(self): - pageText = PrintHeader()+self.name+PrintMenu() - pageText+="
\n" - pageText+="
\n" - pageText+="
  • Connect
  • \n" - pageText+="
  • Disconnect
  • \n" - pageText+="
  • Reset
  • \n" - pageText+="
  • Print
  • \n" - pageText+="
  • Pause
  • \n" - - for i in gPronterPtr.cpbuttons: - pageText+="
  • "+i.label+"
  • \n" - - #for i in gPronterPtr.custombuttons: - # print(str(i)); - - pageText+="
\n" - pageText+="
\n" - pageText+="
\n" - pageText+="
" - pageText+="" - pageText+='' - - pageText+='X Home' - pageText+='Y Home' - pageText+='All Home' - pageText+='Z Home' - pageText+='Y 100' - pageText+='Y 10' - pageText+='Y 1' - pageText+='Y .1' - pageText+='Y -.1' - pageText+='Y -1' - pageText+='Y -10' - pageText+='Y -100' - pageText+='X -100' - pageText+='X 100' - pageText+='X -10' - pageText+='X 10' - pageText+='X -1' - pageText+='X 1' - pageText+='X -.1' - pageText+='X .1' - - pageText+="" - pageText+="
\n" #endxy - pageText+="
" - pageText+="" - pageText+='' - pageText+='Z 10' - pageText+='Z 1' - pageText+='Z .1' - pageText+='Z -.1' - pageText+='Z -1' - pageText+='Z -10' - pageText+="" - #TODO Map Z Moves - pageText+="
\n" #endz - pageText+="
\n" #endgui - pageText+="
\n" #endcontent - pageText+="
\n" - - # Temp Control TBD - # pageText+="
" - # pageText+="
" - # pageText+="" - # pageText+="
" - # pageText+="
" - # pageText+="" - # pageText+="
" - # pageText+="
" - - pageText = pageText+"
File Loaded: "+str(gPronterPtr.filename)+"
" - pageText+="
" - pageText+=PrintFooter() - return pageText - - def AddLog(self, log): - global gWeblog - gWeblog = gWeblog+"
"+log - def AppendLog(self, log): - global gWeblog - gWeblog = re.sub("\n", "
", gWeblog)+log - index.exposed = True - -class WebInterfaceStub(object): - def index(self): - return "Web Interface Must be launched by running Pronterface!" - index.exposed = True - -def KillWebInterfaceThread(): - cherrypy.engine.exit() - -def StartWebInterfaceThread(webInterface): - current_dir = os.path.dirname(os.path.abspath(__file__)) - cherrypy.config.update({'engine.autoreload_on':False}) - cherrypy.config.update(configfile(webInterface.pface.web_config or "http.config")) - conf = {'/css/style.css': {'tools.staticfile.on': True, - 'tools.staticfile.filename': sharedfile('web/css/style.css'), - }, - '/js/asyncCommand.js': {'tools.staticfile.on': True, - 'tools.staticfile.filename': sharedfile('web/js/asyncCommand.js'), - }, - '/images/control_xy.png': {'tools.staticfile.on': True, - 'tools.staticfile.filename': imagefile('control_xy.png'), - }, - '/images/control_z.png': {'tools.staticfile.on': True, - 'tools.staticfile.filename': imagefile('control_z.png'), - }} - cherrypy.config.update(configfile(webInterface.pface.web_config or "http.config")) - cherrypy.quickstart(webInterface, '/', config = conf) - -if __name__ == '__main__': - cherrypy.config.update(configfile("http.config")) - cherrypy.quickstart(WebInterfaceStub()) diff --git a/pronserve.py b/pronserve.py new file mode 100755 index 0000000..f8a5d10 --- /dev/null +++ b/pronserve.py @@ -0,0 +1,403 @@ +#!/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 +import uuid +from operator import itemgetter, attrgetter +from collections import deque + +log = logging.getLogger("root") +__UPLOADS__ = "./uploads" + +# 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 PrintHandler(tornado.web.RequestHandler): + def put(self): + pronserve.do_print() + self.finish("ACK") + +class PauseHandler(tornado.web.RequestHandler): + def put(self): + pronserve.do_pause() + self.finish("ACK") + +class StopHandler(tornado.web.RequestHandler): + def put(self): + pronserve.do_stop() + self.finish("ACK") + +class JobsHandler(tornado.web.RequestHandler): + def post(self): + fileinfo = self.request.files['job'][0] + pronserve.jobs.add(fileinfo['filename'], fileinfo['body']) + self.finish("ACK") + +class JobHandler(tornado.web.RequestHandler): + def delete(self, job_id): + pronserve.jobs.remove(int(job_id)) + self.finish("ACK") + + def put(self, job_id): + args = {'position': int(self.get_argument("job[position]"))} + pronserve.jobs.update(int(job_id), args) + self.finish("ACK") + + +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 on_sensor_changed(self): + for name in ['bed', 'extruder']: + self.send( + sensor_changed= {'name': name, 'value': pronserve.sensors[name]}, + ) + + def on_uncaught_event(self, event_name, data): + listener = "on_%s"%event_name + + if event_name[:4] == 'job_' and event_name != "job_progress_changed": + data = pronserve.jobs.sanitize(data) + self.send({event_name: data}) + + 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.listeners.add(self) + self.write_message({'connected': {'jobs': pronserve.jobs.public_list()}}) + print "WebSocket opened. %i sockets currently open." % len(pronserve.listeners) + + def send(self, dict_args = {}, **kwargs): + args = dict(dict_args.items() + kwargs.items()) + args['timestamp']= time.time() + self.write_message(args) + + def on_message(self, msg): + print "message received: %s"%(msg) + # TODO: the read bit of repl! + # self.write_message("You said: " + msg) + + def on_close(self): + pronserve.listeners.remove(self) + print "WebSocket closed. %i sockets currently open." % len(pronserve.listeners) + +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), + (r"/jobs", JobsHandler), + (r"/jobs/([0-9]*)", JobHandler), + (r"/jobs/print", PrintHandler), + (r"/jobs/pause", PauseHandler), + (r"/stop", StopHandler) +], **settings) + + +# Event Emitter +# ------------------------------------------------- + +class EventEmitter(object): + def __init__(self): + self.listeners = set() + + def fire(self, event_name, content=None): + callback_name = "on_%s" % event_name + for listener in self.listeners: + if hasattr(listener, callback_name): + callback = getattr(listener, callback_name) + if content == None: callback() + else: callback(content) + elif hasattr(listener, "on_uncaught_event"): + listener.on_uncaught_event(event_name, content) + else: + continue + + +# Pronserve: Server-specific functionality +# ------------------------------------------------- + +class Pronserve(pronsole.pronsole, EventEmitter): + + def __init__(self): + pronsole.pronsole.__init__(self) + EventEmitter.__init__(self) + self.settings.sensor_names = {'T': 'extruder', 'B': 'bed'} + self.stdout = sys.stdout + self.ioloop = tornado.ioloop.IOLoop.instance() + self.settings.sensor_poll_rate = 1 # seconds + self.sensors = {'extruder': -1, 'bed': -1} + self.load_default_rc() + self.jobs = PrintJobQueue() + self.job_id_incr = 0 + self.printing_jobs = False + self.current_job = None + self.previous_job_progress = 0 + self.silent = True + services = ({'type': '_construct._tcp', 'port': 8888, 'domain': "local."}) + self.mdns = mdns.publisher().save_group({'name': 'pronserve', 'services': services }) + self.jobs.listeners.add(self) + + def do_print(self): + if self.p.online: + self.printing_jobs = True + + def run_print_queue_loop(self): + # This is a polling work around to the current lack of events in printcore + # A better solution would be one in which a print_finised event could be + # listend for asynchronously without polling. + p = self.p + if self.printing_jobs and p.printing == False and p.paused == False and p.online: + if self.current_job != None: + self.update_job_progress(100) + self.fire("job_finished", self.jobs.sanitize(self.current_job)) + if len(self.jobs.list) > 0: + print "Starting the next print job" + self.current_job = self.jobs.list.popleft() + self.p.startprint(self.current_job['body'].split("\n")) + self.fire("job_started", self.jobs.sanitize(self.current_job)) + else: + print "Finished all print jobs" + self.current_job = None + self.printing_jobs = False + + # Updating the job progress + self.update_job_progress(self.print_progress()) + + #print "print loop" + next_timeout = time.time() + 0.3 + gen.Task(self.ioloop.add_timeout(next_timeout, self.run_print_queue_loop)) + + def update_job_progress(self, progress): + if progress != self.previous_job_progress and self.current_job != None: + self.previous_job_progress = progress + self.fire("job_progress_changed", progress) + + 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") + + 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 print_progress(self): + if(self.p.printing): + return 100*float(self.p.queueindex)/len(self.p.mainqueue) + if(self.sdprinting): + return self.percentdone + return 0 + + + def _receive_sensor_update(self, l): + words = filter(lambda s: s.find(":") > 0, l.split(" ")) + d = dict([ s.split(":") for s in words]) + + # print "sensor update received!" + + for key, value in d.iteritems(): + self.__update_sensor(key, value) + + self.fire("sensor_changed") + + def __update_sensor(self, key, value): + if (key in self.settings.sensor_names) == False: + return + sensor_name = self.settings.sensor_names[key] + self.sensors[sensor_name] = float(value) + + def on_uncaught_event(self, event_name, content=None): + self.fire(event_name, content) + + def log(self, *msg): + msg = ''.join(str(i) for i in msg) + msg.replace("\r", "") + print msg + self.fire("log", {'msg': msg, 'level': "debug"}) + + def write_prompt(self): + None + + +class PrintJobQueue(EventEmitter): + + def __init__(self): + super(PrintJobQueue, self).__init__() + self.list = deque([]) + self.__last_id = 0 + + def public_list(self): + # A sanitized version of list for public consumption via construct + l2 = [] + for job in self.list: + l2.append(self.sanitize(job)) + return l2 + + def sanitize(self, job): + return dict( + id = job["id"], + original_file_name = job["original_file_name"], + rank = job["rank"] + ) + + def order(self): + sorted(self.list, key=lambda job: job['rank']) + + + def add(self, original_file_name, body): + ext = os.path.splitext(original_file_name)[1] + job = dict( + id = self.__last_id, + rank = len(self.list), + original_file_name=original_file_name, + body= body, + ) + self.__last_id += 1 + + self.list.append(job) + print "Added %s"%(original_file_name) + self.fire("job_added", job) + + def display_summary(self): + print "Print Jobs:" + for job in self.list: + print " %i: %s"%(job['id'], job['original_file_name']) + print "" + return True + + def remove(self, job_id): + job = self.find_by_id(job_id) + if job == None: + return False + self.list.remove(job) + print "Print Job Removed" + self.fire("job_removed", job) + + def update(self, job_id, job_attrs): + job = self.find_by_id(job_id) + if job == None: + return False + job['rank'] = job_attrs['position'] + self.order() + print "Print Job Updated" + self.fire("job_updated", job) + + def find_by_id(self, job_id): + for job in self.list: + if job['id'] == job_id: return job + return None + + def fire(self, event_name, content): + self.display_summary() + super(PrintJobQueue, self).fire(event_name, content) + + + +# Server Start Up +# ------------------------------------------------- + +print "Pronserve is starting..." +pronserve = Pronserve() +pronserve.do_connect("") + +time.sleep(1) +pronserve.run_sensor_loop() +pronserve.run_print_queue_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 6bcae83..f2b2303 100755 --- a/pronsole.py +++ b/pronsole.py @@ -77,7 +77,7 @@ def estimate_duration(g): #TODO: # get device caps from firmware: max speed, acceleration/axis (including extruder) # calculate the maximum move duration accounting for above ;) - # print ".... estimating ...." + # self.log(".... estimating ....") for i in g: i = i.split(";")[0] if "G4" in i or "G1" in i: @@ -119,7 +119,7 @@ def estimate_duration(g): if z > lastz: layercount +=1 - #print "layer z: ", lastz, " will take: ", time.strftime('%H:%M:%S', time.gmtime(totalduration-layerbeginduration)) + #self.log("layer z: ", lastz, " will take: ", time.strftime('%H:%M:%S', time.gmtime(totalduration-layerbeginduration))) layerbeginduration = totalduration lastx = x @@ -128,7 +128,7 @@ def estimate_duration(g): laste = e lastf = f - #print "Total Duration: " #, time.strftime('%H:%M:%S', time.gmtime(totalduration)) + #self.log("Total Duration: " #, time.strftime('%H:%M:%S', time.gmtime(totalduration))) return "{0:d} layers, ".format(int(layercount)) + str(datetime.timedelta(seconds = int(totalduration))) def confirm(): @@ -252,6 +252,7 @@ class pronsole(cmd.Cmd): self.settings._bedtemp_abs_cb = self.set_temp_preset self.settings._bedtemp_pla_cb = self.set_temp_preset self.monitoring = 0 + self.silent = False self.helpdict = {} self.helpdict["baudrate"] = _("Communications Speed (default: 115200)") self.helpdict["bedtemp_abs"] = _("Heated Build Platform temp for ABS (default: 110 deg C)") @@ -274,6 +275,9 @@ class pronsole(cmd.Cmd): "macro" : "%(bold)s..>%(normal)s ", "online" : "%(bold)sT:%(extruder_temp_fancy)s %(progress_fancy)s >%(normal)s "} + def log(self, *msg): + print ''.join(str(i) for i in msg) + def promptf(self): """A function to generate prompts so that we can do dynamic prompts. """ if self.in_macro: @@ -323,11 +327,11 @@ class pronsole(cmd.Cmd): if not key.startswith("bed"): self.temps["pla"] = str(self.settings.temperature_pla) self.temps["abs"] = str(self.settings.temperature_abs) - print "Hotend temperature presets updated, pla:%s, abs:%s" % (self.temps["pla"], self.temps["abs"]) + self.log("Hotend temperature presets updated, pla:%s, abs:%s" % (self.temps["pla"], self.temps["abs"])) else: self.bedtemps["pla"] = str(self.settings.bedtemp_pla) self.bedtemps["abs"] = str(self.settings.bedtemp_abs) - print "Bed temperature presets updated, pla:%s, abs:%s" % (self.bedtemps["pla"], self.bedtemps["abs"]) + self.log("Bed temperature presets updated, pla:%s, abs:%s" % (self.bedtemps["pla"], self.bedtemps["abs"])) def scanserial(self): """scan for available ports. return a list of device names.""" @@ -345,7 +349,10 @@ class pronsole(cmd.Cmd): return baselist+glob.glob('/dev/ttyUSB*') + glob.glob('/dev/ttyACM*') +glob.glob("/dev/tty.*")+glob.glob("/dev/cu.*")+glob.glob("/dev/rfcomm*") def online(self): - print "\rPrinter is now online" + self.log("\rPrinter is now online") + self.write_prompt() + + def write_prompt(self): sys.stdout.write(self.promptf()) sys.stdout.flush() @@ -356,7 +363,7 @@ class pronsole(cmd.Cmd): self.help_gcodes() def help_gcodes(self): - print "Gcodes are passed through to the printer as they are" + self.log("Gcodes are passed through to the printer as they are") def complete_macro(self, text, line, begidx, endidx): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "): @@ -386,7 +393,7 @@ class pronsole(cmd.Cmd): setattr(self.__class__, "do_"+self.cur_macro_name, lambda self, largs, macro = macro:macro(self,*largs.split())) setattr(self.__class__, "help_"+self.cur_macro_name, lambda self, macro_name = self.cur_macro_name: self.subhelp_macro(macro_name)) if not self.processing_rc: - print "Macro '"+self.cur_macro_name+"' defined" + self.log("Macro '"+self.cur_macro_name+"' defined") # save it if not self.processing_args: macro_key = "macro "+self.cur_macro_name @@ -398,7 +405,7 @@ class pronsole(cmd.Cmd): macro_def += self.cur_macro_def self.save_in_rc(macro_key, macro_def) else: - print "Empty macro - cancelled" + self.log("Empty macro - cancelled") del self.cur_macro_name, self.cur_macro_def def compile_macro_line(self, line): @@ -413,7 +420,7 @@ class pronsole(cmd.Cmd): def compile_macro(self, macro_name, macro_def): if macro_def.strip() == "": - print "Empty macro - cancelled" + self.log("Empty macro - cancelled") return pycode = "def macro(self,*arg):\n" if "\n" not in macro_def.strip(): @@ -427,7 +434,7 @@ class pronsole(cmd.Cmd): def start_macro(self, macro_name, prev_definition = "", suppress_instructions = False): if not self.processing_rc and not suppress_instructions: - print "Enter macro using indented lines, end with empty line" + self.log("Enter macro using indented lines, end with empty line") self.cur_macro_name = macro_name self.cur_macro_def = "" self.onecmd = self.hook_macro # override onecmd temporarily @@ -438,11 +445,11 @@ class pronsole(cmd.Cmd): if macro_name in self.macros.keys(): delattr(self.__class__, "do_"+macro_name) del self.macros[macro_name] - print "Macro '"+macro_name+"' removed" + self.log("Macro '"+macro_name+"' removed") if not self.processing_rc and not self.processing_args: self.save_in_rc("macro "+macro_name, "") else: - print "Macro '"+macro_name+"' is not defined" + self.log("Macro '"+macro_name+"' is not defined") def do_macro(self, args): if args.strip()=="": self.print_topics("User-defined macros", self.macros.keys(), 15, 80) @@ -450,7 +457,7 @@ class pronsole(cmd.Cmd): arglist = args.split(None, 1) macro_name = arglist[0] if macro_name not in self.macros and hasattr(self.__class__, "do_"+macro_name): - print "Name '"+macro_name+"' is being used by built-in command" + self.log("Name '"+macro_name+"' is being used by built-in command") return if len(arglist) == 2: macro_def = arglist[1] @@ -470,24 +477,24 @@ class pronsole(cmd.Cmd): self.start_macro(macro_name) def help_macro(self): - print "Define single-line macro: macro " - print "Define multi-line macro: macro " - print "Enter macro definition in indented lines. Use {0} .. {N} to substitute macro arguments" - print "Enter python code, prefixed with ! Use arg[0] .. arg[N] to substitute macro arguments" - print "Delete macro: macro /d" - print "Show macro definition: macro /s" - print "'macro' without arguments displays list of defined macros" + self.log("Define single-line macro: macro ") + self.log("Define multi-line macro: macro ") + self.log("Enter macro definition in indented lines. Use {0} .. {N} to substitute macro arguments") + self.log("Enter python code, prefixed with ! Use arg[0] .. arg[N] to substitute macro arguments") + self.log("Delete macro: macro /d") + self.log("Show macro definition: macro /s") + self.log("'macro' without arguments displays list of defined macros") def subhelp_macro(self, macro_name): if macro_name in self.macros.keys(): macro_def = self.macros[macro_name] if "\n" in macro_def: - print "Macro '"+macro_name+"' defined as:" - print self.macros[macro_name]+"----------------" + self.log("Macro '"+macro_name+"' defined as:") + self.log(self.macros[macro_name]+"----------------") else: - print "Macro '"+macro_name+"' defined as: '"+macro_def+"'" + self.log("Macro '"+macro_name+"' defined as: '"+macro_def+"'") else: - print "Macro '"+macro_name+"' is not defined" + self.log("Macro '"+macro_name+"' is not defined") def set(self, var, str): try: @@ -496,29 +503,29 @@ class pronsole(cmd.Cmd): if not self.processing_rc and not self.processing_args: self.save_in_rc("set "+var, "set %s %s" % (var, value)) except AttributeError: - print "Unknown variable '%s'" % var + self.log("Unknown variable '%s'" % var) except ValueError, ve: - print "Bad value for variable '%s', expecting %s (%s)" % (var, repr(t)[1:-1], ve.args[0]) + self.log("Bad value for variable '%s', expecting %s (%s)" % (var, repr(t)[1:-1], ve.args[0])) def do_set(self, argl): args = argl.split(None, 1) if len(args) < 1: for k in [kk for kk in dir(self.settings) if not kk.startswith("_")]: - print "%s = %s" % (k, str(getattr(self.settings, k))) + self.log("%s = %s" % (k, str(getattr(self.settings, k)))) return value = getattr(self.settings, args[0]) if len(args) < 2: try: - print "%s = %s" % (args[0], getattr(self.settings, args[0])) + self.log("%s = %s" % (args[0], getattr(self.settings, args[0]))) except AttributeError: - print "Unknown variable '%s'" % args[0] + self.log("Unknown variable '%s'" % args[0]) return self.set(args[0], args[1]) def help_set(self): - print "Set variable: set " - print "Show variable: set " - print "'set' without arguments displays all variables" + self.log("Set variable: set ") + self.log("Show variable: set ") + self.log("'set' without arguments displays all variables") def complete_set(self, text, line, begidx, endidx): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "): @@ -600,16 +607,16 @@ class pronsole(cmd.Cmd): rci.close() rco.close() #if definition != "": - # print "Saved '"+key+"' to '"+self.rc_filename+"'" + # self.log("Saved '"+key+"' to '"+self.rc_filename+"'") #else: - # print "Removed '"+key+"' from '"+self.rc_filename+"'" + # self.log("Removed '"+key+"' from '"+self.rc_filename+"'") except Exception, e: - print "Saving failed for", key+":", str(e) + self.log("Saving failed for", key+":", str(e)) finally: del rci, rco def preloop(self): - print "Welcome to the printer console! Type \"help\" for a list of available commands." + self.log("Welcome to the printer console! Type \"help\" for a list of available commands.") self.prompt = self.promptf() cmd.Cmd.preloop(self) @@ -626,12 +633,12 @@ class pronsole(cmd.Cmd): try: baud = int(a[1]) except: - print "Bad baud value '"+a[1]+"' ignored" + self.log("Bad baud value '"+a[1]+"' ignored") if len(p) == 0 and not port: - print "No serial ports detected - please specify a port" + self.log("No serial ports detected - please specify a port") return if len(a) == 0: - print "No port specified - connecting to %s at %dbps" % (port, baud) + self.log("No port specified - connecting to %s at %dbps" % (port, baud)) if port != self.settings.port: self.settings.port = port self.save_in_rc("set port", "set port %s" % port) @@ -641,14 +648,14 @@ class pronsole(cmd.Cmd): self.p.connect(port, baud) def help_connect(self): - print "Connect to printer" - print "connect " - print "If port and baudrate are not specified, connects to first detected port at 115200bps" + self.log("Connect to printer") + self.log("connect ") + self.log("If port and baudrate are not specified, connects to first detected port at 115200bps") ports = self.scanserial() if(len(ports)): - print "Available ports: ", " ".join(ports) + self.log("Available ports: ", " ".join(ports)) else: - print "No serial ports were automatically found." + self.log("No serial ports were automatically found.") def complete_connect(self, text, line, begidx, endidx): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "): @@ -662,22 +669,22 @@ class pronsole(cmd.Cmd): self.p.disconnect() def help_disconnect(self): - print "Disconnects from the printer" + self.log("Disconnects from the printer") def do_load(self,l): self._do_load(l) def _do_load(self,l): if len(l)==0: - print "No file name given." + self.log("No file name given.") return - print "Loading file:"+l + self.log("Loading file:"+l) if not(os.path.exists(l)): - print "File not found!" + self.log("File not found!") return self.f = [i.replace("\n", "").replace("\r", "") for i in open(l)] self.filename = l - print "Loaded ", l, ", ", len(self.f)," lines." + self.log("Loaded ", l, ", ", len(self.f)," lines.") def complete_load(self, text, line, begidx, endidx): s = line.split() @@ -690,32 +697,32 @@ class pronsole(cmd.Cmd): return glob.glob("*/")+glob.glob("*.g*") def help_load(self): - print "Loads a gcode file (with tab-completion)" + self.log("Loads a gcode file (with tab-completion)") def do_upload(self, l): if len(l) == 0: - print "No file name given." + self.log("No file name given.") return - print "Loading file:"+l.split()[0] + self.log("Loading file:"+l.split()[0]) if not(os.path.exists(l.split()[0])): - print "File not found!" + self.log("File not found!") return if not self.p.online: - print "Not connected to printer." + self.log("Not connected to printer.") return self.f = [i.replace("\n", "") for i in open(l.split()[0])] self.filename = l.split()[0] - print "Loaded ", l, ", ", len(self.f)," lines." + self.log("Loaded ", l, ", ", len(self.f)," lines.") tname = "" if len(l.split())>1: tname = l.split()[1] else: - print "please enter target name in 8.3 format." + self.log("please enter target name in 8.3 format.") return - print "Uploading as ", tname - print("Uploading "+self.filename) + self.log("Uploading as ", tname) + self.log(("Uploading "+self.filename)) self.p.send_now("M28 "+tname) - print("Press Ctrl-C to interrupt upload.") + self.log(("Press Ctrl-C to interrupt upload.")) self.p.startprint(self.f) try: sys.stdout.write("Progress: 00.0%") @@ -733,16 +740,16 @@ class pronsole(cmd.Cmd): self.recvlisteners+=[self.listfiles] self.p.send_now("M20") time.sleep(0.5) - print "\b\b\b\b\b100%. Upload completed. ", tname, " should now be on the card." + self.log("\b\b\b\b\b100%. Upload completed. ", tname, " should now be on the card.") return except: - print "...interrupted!" + self.log("...interrupted!") self.p.pause() self.p.send_now("M29 "+tname) time.sleep(0.2) self.p.clear = 1 self.p.startprint([]) - print "A partial file named ", tname, " may have been written to the sd card." + self.log("A partial file named ", tname, " may have been written to the sd card.") def complete_upload(self, text, line, begidx, endidx): @@ -756,23 +763,23 @@ class pronsole(cmd.Cmd): return glob.glob("*/")+glob.glob("*.g*") def help_upload(self): - print "Uploads a gcode file to the sd card" + self.log("Uploads a gcode file to the sd card") def help_print(self): if self.f is None: - print "Send a loaded gcode file to the printer. Load a file with the load command first." + self.log("Send a loaded gcode file to the printer. Load a file with the load command first.") else: - print "Send a loaded gcode file to the printer. You have "+self.filename+" loaded right now." + self.log("Send a loaded gcode file to the printer. You have "+self.filename+" loaded right now.") def do_print(self, l): if self.f is None: - print "No file loaded. Please use load first." + self.log("No file loaded. Please use load first.") return if not self.p.online: - print "Not connected to printer." + self.log("Not connected to printer.") return - print("Printing "+self.filename) - print("You can monitor the print with the monitor command.") + self.log(("printing "+self.filename)) + self.log(("You can monitor the print with the monitor command.")) self.p.startprint(self.f) #self.p.pause() #self.paused = True @@ -783,7 +790,7 @@ class pronsole(cmd.Cmd): self.p.send_now("M25") else: if(not self.p.printing): - print "Not printing, cannot pause." + self.log("Not self.log(ing, cannot pause.") return self.p.pause() #self.p.connect()# This seems to work, but is not a good solution. @@ -792,11 +799,11 @@ class pronsole(cmd.Cmd): #self.do_resume(None) def help_pause(self): - print "Pauses a running print" + self.log("Pauses a running print") def do_resume(self, l): if not self.paused: - print "Not paused, unable to resume. Start a print first." + self.log("Not paused, unable to resume. Start a print first.") return self.paused = False if self.sdprinting: @@ -806,7 +813,7 @@ class pronsole(cmd.Cmd): self.p.resume() def help_resume(self): - print "Resumes a paused print." + self.log("Resumes a paused print.") def emptyline(self): pass @@ -825,33 +832,33 @@ class pronsole(cmd.Cmd): def do_ls(self, l): if not self.p.online: - print "Printer is not online. Try connect to it first." + self.log("printer is not online. Try connect to it first.") return self.listing = 2 self.sdfiles = [] self.recvlisteners+=[self.listfiles] self.p.send_now("M20") time.sleep(0.5) - print " ".join(self.sdfiles) + self.log(" ".join(self.sdfiles)) def help_ls(self): - print "lists files on the SD card" + self.log("lists files on the SD card") def waitforsdresponse(self, l): if "file.open failed" in l: - print "Opening file failed." + self.log("Opening file failed.") self.recvlisteners.remove(self.waitforsdresponse) return if "File opened" in l: - print l + self.log(l) if "File selected" in l: - print "Starting print" + self.log("Starting print") self.p.send_now("M24") self.sdprinting = 1 #self.recvlisteners.remove(self.waitforsdresponse) return if "Done printing file" in l: - print l + self.log(l) self.sdprinting = 0 self.recvlisteners.remove(self.waitforsdresponse) return @@ -868,11 +875,11 @@ class pronsole(cmd.Cmd): self.p.reset() def help_reset(self): - print "Resets the printer." + self.log("Resets the printer.") def do_sdprint(self, l): if not self.p.online: - print "Printer is not online. Try connect to it first." + self.log("printer is not online. Try connect to it first.") return self.listing = 2 self.sdfiles = [] @@ -880,17 +887,17 @@ class pronsole(cmd.Cmd): self.p.send_now("M20") time.sleep(0.5) if not (l.lower() in self.sdfiles): - print "File is not present on card. Upload it first" + self.log("File is not present on card. Upload it first") return self.recvlisteners+=[self.waitforsdresponse] self.p.send_now("M23 "+l.lower()) - print "Printing file: "+l.lower()+" from SD card." - print "Requesting SD print..." + self.log("printing file: "+l.lower()+" from SD card.") + self.log("Requesting SD print...") time.sleep(1) def help_sdprint(self): - print "Print a file from the SD card. Tabcompletes with available file names." - print "sdprint filename.g" + self.log("print a file from the SD card. Tabcompletes with available file names.") + self.log("sdprint filename.g") def complete_sdprint(self, text, line, begidx, endidx): if self.sdfiles==[] and self.p.online: @@ -909,32 +916,32 @@ class pronsole(cmd.Cmd): if(tstring!="ok" and not tstring.startswith("ok T") and not tstring.startswith("T:") and not self.listing and not self.monitoring): if tstring[:5] == "echo:": tstring = tstring[5:].lstrip() - print "\r" + tstring.ljust(15) + if self.silent == False: print "\r" + tstring.ljust(15) sys.stdout.write(self.promptf()) sys.stdout.flush() for i in self.recvlisteners: i(l) def help_shell(self): - print "Executes a python command. Example:" - print "! os.listdir('.')" + self.log("Executes a python command. Example:") + self.log("! os.listdir('.')") def default(self, l): if(l[0] in self.commandprefixes.upper()): if(self.p and self.p.online): if(not self.p.loud): - print "SENDING:"+l + self.log("SENDING:"+l) self.p.send_now(l) else: - print "Printer is not online." + self.log("printer is not online.") return elif(l[0] in self.commandprefixes.lower()): if(self.p and self.p.online): if(not self.p.loud): - print "SENDING:"+l.upper() + self.log("SENDING:"+l.upper()) self.p.send_now(l.upper()) else: - print "Printer is not online." + self.log("printer is not online.") return else: cmd.Cmd.default(self, l) @@ -942,6 +949,10 @@ class pronsole(cmd.Cmd): def help_help(self): self.do_help("") + def tempcb(self, l): + if "T:" in l: + self.log(l.replace("\r", "").replace("T", "Hotend").replace("B", "Bed").replace("\n", "").replace("ok ", "")) + def do_gettemp(self, l): if "dynamic" in l: self.dynamic_temp = True @@ -955,7 +966,7 @@ class pronsole(cmd.Cmd): print "Bed: %s/%s" % (self.status.bed_temp, self.status.bed_temp_target) def help_gettemp(self): - print "Read the extruder and bed temperature." + self.log("Read the extruder and bed temperature.") def do_settemp(self, l): try: @@ -970,18 +981,18 @@ class pronsole(cmd.Cmd): return if self.p.online: self.p.send_now("M104 S"+l) - print "Setting hotend temperature to ", f, " degrees Celsius." + self.log("Setting hotend temperature to ", f, " degrees Celsius.") else: - print "Printer is not online." + self.log("printer is not online.") else: - print "You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0." + self.log("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0.") except: - print "You must enter a temperature." + self.log("You must enter a temperature.") def help_settemp(self): - print "Sets the hotend temperature to the value entered." - print "Enter either a temperature in celsius or one of the following keywords" - print ", ".join([i+"("+self.temps[i]+")" for i in self.temps.keys()]) + self.log("Sets the hotend temperature to the value entered.") + self.log("Enter either a temperature in celsius or one of the following keywords") + self.log(", ".join([i+"("+self.temps[i]+")" for i in self.temps.keys()])) def complete_settemp(self, text, line, begidx, endidx): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "): @@ -996,18 +1007,18 @@ class pronsole(cmd.Cmd): if f>=0: if self.p.online: self.p.send_now("M140 S"+l) - print "Setting bed temperature to ", f, " degrees Celsius." + self.log("Setting bed temperature to ", f, " degrees Celsius.") else: - print "Printer is not online." + self.log("printer is not online.") else: - print "You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0." + self.log("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0.") except: - print "You must enter a temperature." + self.log("You must enter a temperature.") def help_bedtemp(self): - print "Sets the bed temperature to the value entered." - print "Enter either a temperature in celsius or one of the following keywords" - print ", ".join([i+"("+self.bedtemps[i]+")" for i in self.bedtemps.keys()]) + self.log("Sets the bed temperature to the value entered.") + self.log("Enter either a temperature in celsius or one of the following keywords") + self.log(", ".join([i+"("+self.bedtemps[i]+")" for i in self.bedtemps.keys()])) def complete_bedtemp(self, text, line, begidx, endidx): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "): @@ -1015,13 +1026,13 @@ class pronsole(cmd.Cmd): def do_move(self, l): if(len(l.split())<2): - print "No move specified." + self.log("No move specified.") return if self.p.printing: - print "Printer is currently printing. Please pause the print before you issue manual commands." + self.log("printer is currently printing. Please pause the print before you issue manual commands.") return if not self.p.online: - print "Printer is not online. Unable to move." + self.log("printer is not online. Unable to move.") return l = l.split() if(l[0].lower()=="x"): @@ -1037,13 +1048,13 @@ class pronsole(cmd.Cmd): feed = self.settings.e_feedrate axis = "E" else: - print "Unknown axis." + self.log("Unknown axis.") return dist = 0 try: dist = float(l[1]) except: - print "Invalid distance" + self.log("Invalid distance") return try: feed = int(l[2]) @@ -1054,11 +1065,11 @@ class pronsole(cmd.Cmd): self.p.send_now("G90") def help_move(self): - print "Move an axis. Specify the name of the axis and the amount. " - print "move X 10 will move the X axis forward by 10mm at ", self.settings.xy_feedrate, "mm/min (default XY speed)" - print "move Y 10 5000 will move the Y axis forward by 10mm at 5000mm/min" - print "move Z -1 will move the Z axis down by 1mm at ", self.settings.z_feedrate, "mm/min (default Z speed)" - print "Common amounts are in the tabcomplete list." + self.log("Move an axis. Specify the name of the axis and the amount. ") + self.log("move X 10 will move the X axis forward by 10mm at ", self.settings.xy_feedrate, "mm/min (default XY speed)") + self.log("move Y 10 5000 will move the Y axis forward by 10mm at 5000mm/min") + self.log("move Z -1 will move the Z axis down by 1mm at ", self.settings.z_feedrate, "mm/min (default Z speed)") + self.log("Common amounts are in the tabcomplete list.") def complete_move(self, text, line, begidx, endidx): if (len(line.split()) == 2 and line[-1] != " ") or (len(line.split()) == 1 and line[-1]==" "): @@ -1078,29 +1089,29 @@ class pronsole(cmd.Cmd): length = 5#default extrusion length feed = self.settings.e_feedrate#default speed if not self.p.online: - print "Printer is not online. Unable to move." + self.log("printer is not online. Unable to move.") return if self.p.printing: - print "Printer is currently printing. Please pause the print before you issue manual commands." + self.log("printer is currently printing. Please pause the print before you issue manual commands.") return ls = l.split() if len(ls): try: length = float(ls[0]) except: - print "Invalid length given." + self.log("Invalid length given.") if len(ls)>1: try: feed = int(ls[1]) except: - print "Invalid speed given." + self.log("Invalid speed given.") if override is not None: length = override feed = overridefeed if length > 0: - print "Extruding %fmm of filament."%(length,) + self.log("Extruding %fmm of filament."%(length,)) elif length <0: - print "Reversing %fmm of filament."%(-1*length,) + self.log("Reversing %fmm of filament."%(-1*length,)) else: "Length is 0, not doing anything." self.p.send_now("G91") @@ -1108,40 +1119,40 @@ class pronsole(cmd.Cmd): self.p.send_now("G90") def help_extrude(self): - print "Extrudes a length of filament, 5mm by default, or the number of mm given as a parameter" - print "extrude - extrudes 5mm of filament at 300mm/min (5mm/s)" - print "extrude 20 - extrudes 20mm of filament at 300mm/min (5mm/s)" - print "extrude -5 - REVERSES 5mm of filament at 300mm/min (5mm/s)" - print "extrude 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)" + self.log("Extrudes a length of filament, 5mm by default, or the number of mm given as a parameter") + self.log("extrude - extrudes 5mm of filament at 300mm/min (5mm/s)") + self.log("extrude 20 - extrudes 20mm of filament at 300mm/min (5mm/s)") + self.log("extrude -5 - REVERSES 5mm of filament at 300mm/min (5mm/s)") + self.log("extrude 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)") def do_reverse(self, l): length = 5#default extrusion length feed = self.settings.e_feedrate#default speed if not self.p.online: - print "Printer is not online. Unable to move." + self.log("printer is not online. Unable to move.") return if self.p.printing: - print "Printer is currently printing. Please pause the print before you issue manual commands." + self.log("printer is currently printing. Please pause the print before you issue manual commands.") return ls = l.split() if len(ls): try: length = float(ls[0]) except: - print "Invalid length given." + self.log("Invalid length given.") if len(ls)>1: try: feed = int(ls[1]) except: - print "Invalid speed given." + self.log("Invalid speed given.") self.do_extrude("", length*-1.0, feed) def help_reverse(self): - print "Reverses the extruder, 5mm by default, or the number of mm given as a parameter" - print "reverse - reverses 5mm of filament at 300mm/min (5mm/s)" - print "reverse 20 - reverses 20mm of filament at 300mm/min (5mm/s)" - print "reverse 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)" - print "reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)" + self.log("Reverses the extruder, 5mm by default, or the number of mm given as a parameter") + self.log("reverse - reverses 5mm of filament at 300mm/min (5mm/s)") + self.log("reverse 20 - reverses 20mm of filament at 300mm/min (5mm/s)") + self.log("reverse 10 210 - extrudes 10mm of filament at 210mm/min (3.5mm/s)") + self.log("reverse -5 - EXTRUDES 5mm of filament at 300mm/min (5mm/s)") def do_exit(self, l): if self.status.extruder_temp_target != 0: @@ -1151,35 +1162,35 @@ class pronsole(cmd.Cmd): if self.status.bed_temp_taret != 0: print "Setting bed temp to 0" self.p.send_now("M140 S0.0") - print "Disconnecting from printer..." + self.log("Disconnecting from printer...") print self.p.printing if self.p.printing: print "Are you sure you want to exit while printing?" print "(this will terminate the print)." if not confirm(): return False - print "Exiting program. Goodbye!" + self.log("Exiting program. Goodbye!") self.p.disconnect() return True def help_exit(self): - print "Disconnects from the printer and exits the program." + self.log("Disconnects from the printer and exits the program.") def do_monitor(self, l): interval = 5 if not self.p.online: - print "Printer is not online. Please connect first." + self.log("printer is not online. Please connect first.") return if not (self.p.printing or self.sdprinting): - print "Printer not printing. Please print something before monitoring." + self.log("Printer not printing. Please print something before monitoring.") return - print "Monitoring printer, use ^C to interrupt." + self.log("Monitoring printer, use ^C to interrupt.") if len(l): try: interval = float(l) except: - print "Invalid period given." - print "Updating values every %f seconds."%(interval,) + self.log("Invalid period given.") + self.log("Updating values every %f seconds."%(interval,)) self.monitoring = 1 prev_msg_len = 0 try: @@ -1197,17 +1208,18 @@ class pronsole(cmd.Cmd): progress = self.percentdone progress = int(progress*10)/10.0 #limit precision prev_msg = preface + str(progress) + "%" - sys.stdout.write("\r" + prev_msg.ljust(prev_msg_len)) - sys.stdout.flush() + if self.silent == False: + sys.stdout.write("\r" + prev_msg.ljust(prev_msg_len)) + sys.stdout.flush() prev_msg_len = len(prev_msg) except KeyboardInterrupt: - print "Done monitoring." + if self.silent == False: print "Done monitoring." self.monitoring = 0 def help_monitor(self): - print "Monitor a machine's temperatures and an SD print's status." - print "monitor - Reports temperature and SD print status (if SD printing) every 5 seconds" - print "monitor 2 - Reports temperature and SD print status (if SD printing) every 2 seconds" + self.log("Monitor a machine's temperatures and an SD print's status.") + self.log("monitor - Reports temperature and SD print status (if SD printing) every 5 seconds") + self.log("monitor 2 - Reports temperature and SD print status (if SD printing) every 2 seconds") def expandcommand(self, c): return c.replace("$python", sys.executable) @@ -1215,31 +1227,31 @@ class pronsole(cmd.Cmd): def do_skein(self, l): l = l.split() if len(l) == 0: - print "No file name given." + self.log("No file name given.") return settings = 0 if(l[0]=="set"): settings = 1 else: - print "Skeining file:"+l[0] + self.log("Skeining file:"+l[0]) if not(os.path.exists(l[0])): - print "File not found!" + self.log("File not found!") return try: import shlex if(settings): param = self.expandcommand(self.settings.sliceoptscommand).replace("\\", "\\\\").encode() - print "Entering slicer settings: ", param + self.log("Entering slicer settings: ", param) subprocess.call(shlex.split(param)) else: param = self.expandcommand(self.settings.slicecommand).encode() - print "Slicing: ", param + self.log("Slicing: ", param) params = [i.replace("$s", l[0]).replace("$o", l[0].replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")).encode() for i in shlex.split(param.replace("\\", "\\\\").encode())] subprocess.call(params) - print "Loading sliced file." + self.log("Loading sliced file.") self.do_load(l[0].replace(".stl", "_export.gcode")) except Exception, e: - print "Skeinforge execution failed: ", e + self.log("Skeinforge execution failed: ", e) def complete_skein(self, text, line, begidx, endidx): s = line.split() @@ -1252,18 +1264,18 @@ class pronsole(cmd.Cmd): return glob.glob("*/")+glob.glob("*.stl") def help_skein(self): - print "Creates a gcode file from an stl model using the slicer (with tab-completion)" - print "skein filename.stl - create gcode file" - print "skein filename.stl view - create gcode file and view using skeiniso" - print "skein set - adjust slicer settings" + self.log("Creates a gcode file from an stl model using the slicer (with tab-completion)") + self.log("skein filename.stl - create gcode file") + self.log("skein filename.stl view - create gcode file and view using skeiniso") + self.log("skein set - adjust slicer settings") def do_home(self, l): if not self.p.online: - print "Printer is not online. Unable to move." + self.log("printer is not online. Unable to move.") return if self.p.printing: - print "Printer is currently printing. Please pause the print before you issue manual commands." + self.log("printer is currently printing. Please pause the print before you issue manual commands.") return if "x" in l.lower(): self.p.send_now("G28 X0") @@ -1278,32 +1290,26 @@ class pronsole(cmd.Cmd): self.p.send_now("G92 E0") def help_home(self): - print "Homes the printer" - print "home - homes all axes and zeroes the extruder(Using G28 and G92)" - print "home xy - homes x and y axes (Using G28)" - print "home z - homes z axis only (Using G28)" - print "home e - set extruder position to zero (Using G92)" - print "home xyze - homes all axes and zeroes the extruder (Using G28 and G92)" + self.log("Homes the printer") + self.log("home - homes all axes and zeroes the extruder(Using G28 and G92)") + self.log("home xy - homes x and y axes (Using G28)") + self.log("home z - homes z axis only (Using G28)") + self.log("home e - set extruder position to zero (Using G92)") + self.log("home xyze - homes all axes and zeroes the extruder (Using G28 and G92)") def parse_cmdline(self, args): import getopt - opts, args = getopt.getopt(args, "c:e:hw", ["conf = ", "config = ", "help", "web", "web-config = ", "web-auth-config = "]) + opts, args = getopt.getopt(args, "c:e:hw", ["conf = ", "config = ", "help"]) for o, a in opts: - #print repr((o, a)) + #self.log(repr((o, a))) if o in ("-c", "--conf", "--config"): self.load_rc(a) - elif o in ("-w", "--web"): - self.webrequested = True - elif o == "--web-config": - self.web_config = a - elif o == "--web-auth-config": - self.web_auth_config = a elif o in ("-h", "--help"): - print "Usage: "+sys.argv[0]+' [-c filename [-c filename2 ... ] ] [-e "command" ...]' - print " -c | --conf | --config - override startup .pronsolerc file" - print " may chain config files, settings auto-save will go into last file in the chain" - print ' -e - executes command after configuration/.pronsolerc is loaded' - print " macros/settings from these commands are not autosaved" + self.log("Usage: "+sys.argv[0]+' [-c filename [-c filename2 ... ] ] [-e "command" ...]') + self.log(" -c | --conf | --config - override startup .pronsolerc file") + self.log(" may chain config files, settings auto-save will go into last file in the chain") + self.log(' -e - executes command after configuration/.pronsolerc is loaded') + self.log(" macros/settings from these commands are not autosaved") sys.exit() if not self.rc_loaded: self.load_default_rc() diff --git a/pronterface.py b/pronterface.py index ceb87dc..356dff2 100755 --- a/pronterface.py +++ b/pronterface.py @@ -196,21 +196,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): self.predisconnect_layer = None self.hsetpoint = 0.0 self.bsetpoint = 0.0 - self.webInterface = None - if self.webrequested: - try : - import cherrypy - from printrun import webinterface - try: - self.webInterface = webinterface.WebInterface(self) - self.webThread = threading.Thread(target = webinterface.StartWebInterfaceThread, args = (self.webInterface, )) - self.webThread.start() - except: - print _("Failed to start web interface") - traceback.print_exc(file = sys.stdout) - self.webInterface = None - except: - print _("CherryPy is not installed. Web Interface Disabled.") if self.filename is not None: self.do_load(self.filename) @@ -364,8 +349,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): print _("You cannot set negative temperatures. To turn the hotend off entirely, set its temperature to 0.") except Exception, x: print _("You must enter a temperature. (%s)") % (repr(x),) - if self.webInterface: - self.webInterface.AddLog("You must enter a temperature. (%s)" % (repr(x),)) def do_bedtemp(self, l = ""): try: @@ -382,16 +365,10 @@ class PronterWindow(MainWindow, pronsole.pronsole): self.setbedgui(f) else: print _("Printer is not online.") - if self.webInterface: - self.webInterface.AddLog("Printer is not online.") else: print _("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0.") - if self.webInterface: - self.webInterface.AddLog("You cannot set negative temperatures. To turn the bed off entirely, set its temperature to 0.") except Exception, x: print _("You must enter a temperature. (%s)") % (repr(x),) - if self.webInterface: - self.webInterface.AddLog("You must enter a temperature.") def end_macro(self): pronsole.pronsole.end_macro(self) @@ -411,8 +388,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): self.delete_macro(macro_name) return print _("Cancelled.") - if self.webInterface: - self.webInterface.AddLog("Cancelled.") return self.cur_macro_name = macro_name self.cur_macro_def = definition @@ -452,8 +427,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): projectlayer.setframe(self,self.p).Show() else: print _("Printer is not online.") - if self.webInterface: - self.webInterface.AddLog("Printer is not online.") def popmenu(self): self.menustrip = wx.MenuBar() @@ -530,8 +503,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): old_def = self.macros[macro] elif len([c for c in macro.encode("ascii", "replace") if not c.isalnum() and c != "_"]): print _("Macro name may contain only ASCII alphanumeric symbols and underscores") - if self.webInterface: - self.webInterface.AddLog("Macro name may contain only alphanumeric symbols and underscores") return elif hasattr(self.__class__, "do_"+macro): print _("Name '%s' is being used by built-in command") % macro @@ -713,8 +684,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): def help_button(self): print _('Defines custom button. Usage: button "title" [/c "colour"] command') - if self.webInterface: - self.webInterface.AddLog('Defines custom button. Usage: button "title" [/c "colour"] command') def do_button(self, argstr): def nextarg(rest): @@ -737,8 +706,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): command = argstr.strip() if num<0 or num>=64: print _("Custom button number should be between 0 and 63") - if self.webInterface: - self.webInterface.AddLog("Custom button number should be between 0 and 63") return while num >= len(self.custombuttons): self.custombuttons.append(None) @@ -1004,8 +971,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): self.cur_button = None except: print _("event object missing") - if self.webInterface: - self.webInterface.AddLog("event object missing") self.cur_button = None raise @@ -1025,9 +990,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): except: pass self.Destroy() - if self.webInterface: - from printrun import webinterface - webinterface.KillWebInterfaceThread() def do_monitor(self, l = ""): if l.strip()=="": @@ -1040,17 +1002,11 @@ class PronterWindow(MainWindow, pronsole.pronsole): wx.CallAfter(self.monitorbox.SetValue, self.monitor_interval>0) except: print _("Invalid period given.") - if self.webInterface: - self.webInterface.AddLog("Invalid period given.") self.setmonitor(None) if self.monitor: print _("Monitoring printer.") - if self.webInterface: - self.webInterface.AddLog("Monitoring printer.") else: print _("Done monitoring.") - if self.webInterface: - self.webInterface.AddLog("Done monitoring.") def setmonitor(self, e): self.monitor = self.monitorbox.GetValue() @@ -1065,8 +1021,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): except: print "attempted to write invalid text to console" pass - if self.webInterface: - self.webInterface.AppendLog(text) def setloud(self,e): self.p.loud=e.IsChecked() @@ -1229,8 +1183,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): import shlex param = self.expandcommand(self.settings.slicecommand).encode() print "Slicing: ", param - if self.webInterface: - self.webInterface.AddLog("Slicing: "+param) pararray = [i.replace("$s", self.filename).replace("$o", self.filename.replace(".stl", "_export.gcode").replace(".STL", "_export.gcode")).encode() for i in shlex.split(param.replace("\\", "\\\\").encode())] #print pararray self.skeinp = subprocess.Popen(pararray, stderr = subprocess.STDOUT, stdout = subprocess.PIPE) @@ -1242,8 +1194,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): self.stopsf = 1 except: print _("Failed to execute slicing software: ") - if self.webInterface: - self.webInterface.AddLog("Failed to execute slicing software: ") self.stopsf = 1 traceback.print_exc(file = sys.stdout) @@ -1336,8 +1286,6 @@ class PronterWindow(MainWindow, pronsole.pronsole): Xtot, Ytot, Ztot, Xmin, Xmax, Ymin, Ymax, Zmin, Zmax = pronsole.measurements(self.f) print pronsole.totalelength(self.f), _("mm of filament used in this print\n") print _("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot) - if self.webInterface: - self.webInterface.AddLog(_("the print goes from %f mm to %f mm in X\nand is %f mm wide\n") % (Xmin, Xmax, Xtot)) print _("the print goes from %f mm to %f mm in Y\nand is %f mm wide\n") % (Ymin, Ymax, Ytot) print _("the print goes from %f mm to %f mm in Z\nand is %f mm high\n") % (Zmin, Zmax, Ztot) try: 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..8de149c --- /dev/null +++ b/server/static/css/inspect.css @@ -0,0 +1,54 @@ +.sensors +{ + margin-top: 15px; +} + +.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: 200px; + overflow-y: scroll; +} + +.console pre +{ + border: 0px; + margin: 0px; + padding: 0px; +} + +#temperature-graph +{ + height: 200px; +} + +#print-job-panel +{ + margin: 0px; +} + +.job-pogress +{ + margin: 80px 0; + font-size: 40px; + line-height: 40px; + text-align: center; +} \ 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..957edaf --- /dev/null +++ b/server/static/js/inspect.js @@ -0,0 +1,145 @@ +(function() { + var $console; + + var windowFocus = true; + + $(window).focus(function() { + windowFocus = true; + //if ($console) $console.append("Window refocused, restarting graph.\n"); + $(".focus-lost-overlay").addClass("out").removeClass("in").delay(1000).hide(); + }).blur(function() { + windowFocus = false; + //if ($console) $console.append("Window's focus, lost stopping graph...\n"); + $(".focus-lost-overlay") + .stop(true,true) + .show() + .addClass("in") + .removeClass("out"); + }.debounce()); + + 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...\n") + onConnect(ws) + }); + }; + + var updateSensorsUi = function() { + $(".sensors .val").each(function() { + $(this).html($(this).data("val")||"xx.x"); + }) + }.throttle(800); + + var graph = null; + var graphData = []; + var graphResolution = 40; + + var updateGraphData = function(current) { + current.time = Date.now(); + if(graphData.length == graphResolution) graphData.shift(); + graphData.push(current); + } + + var updateGraphUi = function(current) { + if(graph == null) + { + graph = new Morris.Line({ + // ID of the element in which to draw the chart. + element: "temperature-graph", + // Chart data records -- each entry in this array corresponds to a point on + // the chart. + data: graphData, + // The name of the data record attribute that contains x-values. + xkey: 'timestamp', + // A list of names of data record attributes that contain y-values. + ykeys: ['extruder', 'bed'], + // Labels for the ykeys -- will be displayed when you hover over the + // chart. + labels: ['extruder °C', 'bed °C'], + hideHover: 'always', + ymax: 'auto 250', + //pointSize: 0, + //parseTime: false, + xLabels: "decade" + }); + } + else + { + graph.setData(graphData); + } + } + + var updateUi = function(msg) { + if(windowFocus == false) return; + updateSensorsUi(); + updateGraphUi(); + } + + var onConnect = function(ws) { + ws.onopen = function() + { + $console.append("Connected.\n"); + // Web Socket is connected, send data using send() + + }; + var nextGraphPoint = {}; + ws.onmessage = function (evt) + { + msg = JSON.parse(evt.data) + if(msg.sensor_changed != undefined) + { + var sensorNames = ["bed", "extruder"]; + for (var i = 0; i < sensorNames.length; i++) + { + var name = msg.sensor_changed.name; + var val = parseFloat(msg.sensor_changed.value); + nextGraphPoint[name] = val; + $("."+name+" .val").data("val", val.format(1)) + } + if(nextGraphPoint.bed != undefined && nextGraphPoint.extruder != undefined) + { + nextGraphPoint.timestamp = msg.timestamp + updateGraphData(nextGraphPoint); + nextGraphPoint = {}; + } + requestAnimationFrame(updateUi); + } + else if (msg.job_progress_changed != undefined) + { + val = Math.round(parseFloat(msg.job_progress_changed)*10)/10; + $(".job-pogress .val").html(val); + } + else + { + console.log($consoleWrapper.scrollTop() - $console.innerHeight()) + var atBottom = $consoleWrapper.scrollTop() - $console.innerHeight() > -220; + $console.append(evt.data + "\n"); + if (atBottom) + { + $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..fe2819f --- /dev/null +++ b/server/templates/index.html @@ -0,0 +1,24 @@ + + + + Pronserve + + + + + + +
+

+ Your printer just got a whole lot better. +

+

+ Pronserve is ready to print. Why not try it out with + Inspector or + 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..dd90da1 --- /dev/null +++ b/server/templates/inspect.html @@ -0,0 +1,57 @@ + + + + Pronserve Inspector + + + + + + + +
+
+
+

+ Pronserve Inspector +

+

Console

+
+
+            Connecting...
+          
+
+
+ +
+
+
+ Extruder: xx.x°C +
+
+ Bed: xx.x°C +
+
+

Temperature

+
+
+
+
+
+
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/testfiles/quick-test.gcode b/testfiles/quick-test.gcode new file mode 100644 index 0000000..0ee6b3d --- /dev/null +++ b/testfiles/quick-test.gcode @@ -0,0 +1,101 @@ +; THIS IS A TZST. DO NOT ATTZMPT TO PRINT THIS FILZ. +; gZnZratZd by Slic3r 0.9.3-dZv on 2012-09-02 at 04:02:31 + +; layZr_hZight = 0.4 +; pZrimZtZrs = 3 +; solid_layZrs = 3 +; fill_dZnsity = 0.4 +; pZrimZtZr_spZZd = 30 +; infill_spZZd = 60 +; travZl_spZZd = 130 +; scalZ = 1 +; nozzlZ_diamZtZr = 0.5 +; filamZnt_diamZtZr = 3 +; Zxtrusion_multipliZr = 1 +; singlZ wall width = 0.53mm +; first layZr singlZ wall width = 0.80mm + +M104 S200 ; sZt tZmpZraturZ +;G28 ; homZ all axZs +;M109 S200 ; wait for tZmpZraturZ to bZ rZachZd +G90 ; usZ absolutZ coordinatZs +G21 ; sZt units to millimZtZrs +G92 Z0 +M82 ; usZ absolutZ distancZs for Zxtrusion +G1 Z0.400 F71800.000 +G1 X75.725 Y86.681 +G1 F1800.000 Z1.00000 +G1 X87.905 Y75.241 F1040.000 Z1.69560 +G1 X88.365 Y74.871 Z1.72017 +G1 X88.865 Y74.541 Z1.74511 +G1 X89.395 Y74.261 Z1.77006 +G1 X89.945 Y74.031 Z1.79488 +G1 X90.225 Y73.931 Z1.80726 +G1 X90.805 Y73.771 Z1.83230 +G1 X92.375 Y73.501 Z1.89862 +G1 X92.935 Y73.471 Z1.92196 +G1 X109.165 Y73.961 Z2.59789 +G1 X109.475 Y73.991 Z2.61085 +G1 X110.105 Y74.101 Z2.63747 +G1 X110.715 Y74.271 Z2.66383 +G1 X111.795 Y74.681 Z2.71192 +G1 X112.355 Y74.951 Z2.73780 +G1 X112.875 Y75.271 Z2.76322 +G1 X113.135 Y75.451 Z2.77638 +G1 X113.615 Y75.841 Z2.80213 +G1 X124.855 Y87.841 Z3.48656 +G1 X125.485 Y88.631 Z3.52863 +G1 X125.975 Y89.351 Z3.56488 +G1 X126.385 Y90.111 Z3.60083 +G1 X126.605 Y90.651 Z3.62510 +G1 X126.775 Y91.201 Z3.64906 +G1 X126.905 Y91.771 Z3.67340 +G1 X126.975 Y92.341 Z3.69731 +G1 X127.005 Y92.921 Z3.72148 +G1 X126.325 Y109.851 Z4.42681 +G1 X126.255 Y110.391 Z4.44947 +G1 X126.145 Y110.921 Z4.47201 +G1 X125.995 Y111.441 Z4.49453 +G1 X125.805 Y111.951 Z4.51719 +G1 X125.575 Y112.441 Z4.53972 +G1 X125.165 Y113.131 Z4.57313 +G1 X124.835 Y113.571 Z4.59603 +G1 X124.485 Y113.971 Z4.61815 +G1 X124.095 Y114.351 Z4.64082 +G1 X123.885 Y114.531 Z4.65233 +G1 X123.005 Y115.151 Z4.69715 +G1 X122.525 Y115.401 Z4.71967 +G1 X96.195 Y125.661 Z5.89600 +G1 X95.515 Y125.881 Z5.92575 +G1 X94.385 Y126.141 Z5.97402 +G1 X91.335 Y126.551 Z6.10213 +G1 X91.055 Y126.561 Z6.11379 +G1 X90.775 Y126.561 Z6.12545 +G1 X90.215 Y126.521 Z6.14882 +G1 X89.375 Y126.371 Z6.18434 +G1 X88.835 Y126.221 Z6.20767 +G1 X88.305 Y126.021 Z6.23125 +G1 X87.795 Y125.781 Z6.25471 +G1 X87.075 Y125.341 Z6.28984 +G1 X86.415 Y124.811 Z6.32507 +G1 X75.155 Y112.801 Z7.01038 +G1 X74.955 Y112.561 Z7.02339 +G1 X74.595 Y112.041 Z7.04972 +G1 X73.825 Y110.701 Z7.11405 +G1 X73.695 Y110.441 Z7.12615 +G1 X73.475 Y109.911 Z7.15004 +G1 X73.305 Y109.361 Z7.17400 +G1 X73.175 Y108.801 Z7.19794 +G1 X73.095 Y108.231 Z7.22190 +G1 X73.065 Y107.651 Z7.24607 +G1 X73.615 Y91.111 Z7.93497 +G1 X73.725 Y90.281 Z7.96982 +G1 X73.785 Y90.011 Z7.98134 +G1 X74.035 Y89.211 Z8.01623 +G1 X74.255 Y88.701 Z8.03935 +G1 X74.515 Y88.201 Z8.06281 +G1 X74.815 Y87.731 Z8.08602 +G1 X75.335 Y87.081 Z8.12067 +G1 X75.640 Y86.766 Z8.13893 + + diff --git a/web/css/style.css b/web/css/style.css deleted file mode 100644 index 427f6c3..0000000 --- a/web/css/style.css +++ /dev/null @@ -1,206 +0,0 @@ -#title -{ - text-align:center; - color:red; -} - -#mainmenu -{ -margin: 0; -padding: 0 0 20px 10px; -border-bottom: 1px solid #000; -} -#mainmenu ul, #mainmenu li -{ -margin: 0; -padding: 0; -display: inline; -list-style-type: none; -} - -#mainmenu a:link, #mainmenu a:visited -{ -float: left; -line-height: 14px; -font-weight: bold; -margin: 0 10px 4px 10px; -text-decoration: none; -color: #999; -} - -#mainmenu a:link#current, #mainmenu a:visited#current, #mainmenu a:hover -{ -border-bottom: 4px solid #000; -padding-bottom: 2px; -background: transparent; -color: #000; -} - -#mainmenu a:hover { color: #000; } - -#content{ -padding-top: 25px; -} -#controls{ - float:left; - padding:0 0 1em 0; - overflow:hidden; - width:71%; /* right column content width */ - left:102%; /* 100% plus left column left padding */ -} -#control_xy{ - display:inline; -} - -#control_z{ - display:inline; - position:absolute; -} - -#gui{ - float:left; - padding:0 0 1em 0; - overflow:hidden; - width:21%; /* left column content width (column width minus left and right padding) */ - left:6%; /* (right column left and right padding) plus (left column left padding) */ - -} -#controls -{ - width:21%; /* Width of left column content (column width minus padding on either side) */ - left:31%; /* width of (right column) plus (center column left and right padding) plus (left column left padding) */ - -} - -#controls ul -{ -list-style: none; -margin: 0px; -padding: 0px; -border: none; -} - -#controls ul li -{ -margin: 0px; -padding: 0px; -} - -#controls ul li a -{ -font-size: 80%; -display: block; -border-bottom: 1px dashed #C39C4E; -padding: 5px 0px 2px 4px; -text-decoration: none; -color: #666666; -width:160px; -} - -#controls ul li a:hover, #controls ul li a:focus -{ -color: #000000; -background-color: #eeeeee; -} - -#settings -{ -margin: 0px; -padding-top: 50px; -border: none; -} - -#settings table -{ - font-family: verdana,arial,sans-serif; - font-size:11px; - color:#333333; - border-width: 1px; - border-color: #999999; - border-collapse: collapse; -} -#settings table th { - background-color:#c3dde0; - border-width: 1px; - padding: 8px; - border-style: solid; - border-color: #a9c6c9; -} -#settings table tr { - background-color:#d4e3e5; -} -#settings table td { - border-width: 1px; - padding: 8px; - border-style: solid; - border-color: #a9c6c9; -} - -#status{ - -} - -#console{ - -} - -#file{ - position:relative; - float:left; - width:100%; - height:20px; /* Height of the footer */ - background:#eee; -} - -#logframe{ - -} - -#temp{ -} -#tempmenu -{ -padding: 0 0 10px 10px; -position: relative; -float: left; -width: 100%; -} -#tempmenu ul, #tempmenu li -{ -margin: 0; -display: inline; -list-style-type: none; -} - -#tempmenu b -{ -padding-top: 4px; -float: left; -line-height: 14px; -font-weight: bold; -color: #888; -margin: 0 10px 4px 10px; -text-decoration: none; -color: #999; -} - -#tempmenu a:link, #tempmenu a:visited -{ -float: left; -border-bottom: 1px solid #000; -line-height: 14px; -font-weight: bold; -margin: 0 10px 4px 10px; -text-decoration: none; -color: #999; -} - -#tempmenu a:link#tempmenu, #tempmenu a:visited#current, #tempmenu a:hover -{ -border-bottom: 2px solid #000; -padding-bottom: 2px; -background: transparent; -color: #000; -} - -#tempmenu a:hover { color: #000; } \ No newline at end of file diff --git a/web/js/asyncCommand.js b/web/js/asyncCommand.js deleted file mode 100644 index f37ec8c..0000000 --- a/web/js/asyncCommand.js +++ /dev/null @@ -1,79 +0,0 @@ -function pronterfaceWebInterface_setup(){ - pronterfaceWebInterface_attachAsync(); -} - -function pronterfaceWebInterface_attachAsync(){ - - var list = []; - if(document.getElementsByClassName){ - list = document.getElementsByClassName('command'); - }else if(document.getElementsByTagName){ - list = document.getElementsByTagName('a'); - list.concat( document.getElementsByTagName('area') ); - //TODO filter list via checking the className attributes - }else{ - console && console.error && console.error('unable to gather list of elements'); - return false; - } - - for(var i=0; i < list.length; i++){ - list[i].addEventListener && list[i].addEventListener( 'click', function(e){return pronterfaceWebInterface_asyncCommand(null, e);}, true ); - list[i].attachEvent && list[i].attachEvent( 'onclick', function(e){return pronterfaceWebInterface_asyncCommand(null, e);} ); - } - - return true; -} - - -function pronterfaceWebInterface_asyncCommand( urlOrElement, event ){ - - if( ! urlOrElement && event.target) - urlOrElement = event.target; - - var url = null; - if( typeof urlOrElement == 'string' ){ - url = urlOrElement; - }else{ - url = urlOrElement&&urlOrElement.href; - } - - if( typeof url != 'string' ){ - console && console.error && console.error('url not a string', urlOrElement, url); - return true; - } - - var httpRequest; - if (window.XMLHttpRequest) { // Mozilla, Safari, ... - httpRequest = new XMLHttpRequest(); - } else if (window.ActiveXObject) { // IE 8 and older - httpRequest = new ActiveXObject("Microsoft.XMLHTTP"); - } - - if( ! httpRequest ){ - alert('no AJAX available?'); - // follow link - return true; - } - - //onreadystatechange - //onerror - httpRequest.open( 'GET', url, true); - httpRequest.send(null); - - // don't follow link - if( event ){ - event.stopImmediatePropagation && event.stopImmediatePropagation(); - event.defaultPrevented = true; - event.preventDefault && event.preventDefault(); - } - return false; -} - - -if (document.addEventListener) { - document.addEventListener("DOMContentLoaded", pronterfaceWebInterface_setup, false); -} else if (document.attachEvent) { - document.attachEvent("onreadystatechange", pronterfaceWebInterface_setup); -} else { - document.onload = pronterfaceWebInterface_setup; -}
settingvalue
Build Dimenstions"+str(gPronterPtr.settings.build_dimensions)+"
Last Bed Temp"+str(gPronterPtr.settings.last_bed_temperature)+"
Last File Path"+gPronterPtr.settings.last_file_path+"
Last Temperature"+str(gPronterPtr.settings.last_temperature)+"
Preview Extrusion Width"+str(gPronterPtr.settings.preview_extrusion_width)+"
Filename"+str(gPronterPtr.filename)+"