Commit cebc3212 authored by D1plo1d's avatar D1plo1d

Working towards construct protocol 0.3. Refactoring the construct server as a reusable library.

parent 168163d5
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
#!/usr/bin/env python #!/usr/bin/env python
import glob, os, time, datetime, sys, codecs, random, textwrap, re, traceback
import logging, argparse, tornado.ioloop, printcore, pronsole
import tornado.ioloop
import tornado.web
import tornado.websocket
from tornado.web import asynchronous
from tornado import gen
import tornado.httpserver
import uuid
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 pprint import pprint from pprint import pprint
import printcore from construct_server.construct_server import ConstructServer
from printrun import gcoder from construct_server.event_emitter import EventEmitter
import pronsole
from printrun.server import basic_auth
import random
import json
import textwrap
import SocketServer
import socket
import pybonjour
import atexit
import uuid
import re
import traceback
import argparse
from operator import itemgetter, attrgetter
import codecs
sys.stdout = codecs.getwriter('utf8')(sys.stdout)
log = logging.getLogger("root")
__UPLOADS__ = "./uploads"
CONSTRUCT_PROTOCOL_VERSION = [0,2,0]
# 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): from printrun import gcoder
"""
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 sys.stdout = codecs.getwriter('utf8')(sys.stdout)
auth = basic_auth.authenticate('auth_realm', authenticator, user_extractor)
#@interceptor(auth)
log = logging.getLogger("root")
# Routing
# -------------------------------------------------
class RootHandler(tornado.web.RequestHandler): class RootHandler(tornado.web.RequestHandler):
def get(self): def get(self):
self.render("index.html") self.render("index.html")
class PrintHandler(tornado.web.RequestHandler):
def put(self):
prontserve.do_print()
self.finish("ACK")
class PauseHandler(tornado.web.RequestHandler):
def put(self):
prontserve.do_pause()
self.finish("ACK")
class StopHandler(tornado.web.RequestHandler):
def put(self):
prontserve.do_stop()
self.finish("ACK")
from cStringIO import StringIO
@tornado.web.stream_body
class JobsHandler(tornado.web.RequestHandler):
def post(self):
self.read_bytes = 0
self.total_bytes = self.request.content_length
self.file_str = StringIO()
print self.request
session_uuid = self.get_argument("session_uuid", None)
print "me"
print session_uuid
print "them"
self.websocket = None
for c in ConstructSocketHandler.clients:
print c.session_uuid
if c.session_uuid == session_uuid: self.websocket = c
self.request.request_continue()
self.read_chunks()
def read_chunks(self, chunk=''):
self.read_bytes += len(chunk)
self.file_str.write(chunk)
if chunk: self.process_chunk()
chunk_length = min(100000, self.request.content_length - self.read_bytes)
if chunk_length > 0:
self.request.connection.stream.read_bytes(
chunk_length, self.read_chunks)
else:
self.request._on_request_body(self.file_str.getvalue(), self.uploaded)
def process_chunk(self):
print self.get_argument("session_uuid", None)
print "bytes: (%i / %i)"%(self.read_bytes, self.total_bytes)
msg = {'uploaded': self.read_bytes, 'total': self.total_bytes}
if self.websocket != None:
self.websocket.send(job_upload_progress_changed = msg)
def uploaded(self):
fileinfo = self.request.files['job'][0]
prontserve.do_add_job(fileinfo['filename'], fileinfo['body'])
self.finish("ACK")
class JobHandler(tornado.web.RequestHandler):
def delete(self, job_id):
prontserve.do_rm_job(job_id)
self.finish("ACK")
def put(self, job_id):
args = {'position': int(self.get_argument("job[position]"))}
prontserve.do_change_job(job_id, **args)
self.finish("ACK")
class InspectHandler(tornado.web.RequestHandler): class InspectHandler(tornado.web.RequestHandler):
def prepare(self): def prepare(self):
auth(self, None) construct_auth(self, None)
def get(self): def get(self):
self.render("inspect.html") self.render("inspect.html")
#class EchoWebSocketHandler(tornado.web.RequestHandler):
class ConstructSocketHandler(tornado.websocket.WebSocketHandler):
clients = []
def on_sensor_changed(self):
for name in ['bed', 'extruder']:
self.send(
sensor_changed= {'name': name, 'value': prontserve.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 = prontserve.jobs.sanitize(data)
self.send({event_name: data})
def _execute(self, transforms, *args, **kwargs):
self.authorized = socket_auth(self)
super(ConstructSocketHandler, self)._execute(transforms, *args, **kwargs)
def select_subprotocol(self, subprotocols):
# Chooses a compatible construct protocol from the list sent by the
# client.
# The Construct Protocol uses semantic version v2.0.0 so any minor version
# w/ a specific major version will be compatible with all minor versions
# before it within that same major version.
# The only exception to this rule is major version 0.
# Version 0 protocols are unstable and can break compatibility more often.
# We are only going to cause backwards incompatibility in 0.x minor version
# See http://semver.org/
server_v = CONSTRUCT_PROTOCOL_VERSION
compatible_v = [-1, -1]
for p in list(subprotocols):
regex = '^construct\.text\.([0-9]+)\.?([0-9]+)'
client_v = [int(s) for s in list(re.search(regex, p).groups())]
pprint(client_v)
if client_v[0] == server_v[0] and client_v[1] <= server_v[1]:
if client_v[1] > compatible_v[1]: compatible_v = client_v
print subprotocols
print compatible_v
# On incompatibility: Return a BS version and sending an error once the
# connection opens.
self.client_versions = subprotocols
self.compatible = compatible_v[1] > -1
pprint(self.compatible)
print self._protocol_str(compatible_v)
if not self.compatible: return subprotocols[0]
return self._protocol_str(compatible_v)
def _protocol_str(self, v):
return "construct.text.%i.%i"%(v[0], v[1])
def open(self):
if not self.authorized:
self._error(
message= "Incorrect password or username",
type= 'unauthorized'
)
if not self.compatible:
v = self._protocol_str(CONSTRUCT_PROTOCOL_VERSION)
msg = """
Incompatible Construct Protocol version.
Server version: %s
Your version(s): %s
"""%(v, str(self.client_versions))
self._error(message= msg, type= 'incompatible_protocol_version')
if not (self.authorized and self.compatible): return self.stream.close()
self.session_uuid = str(uuid.uuid4())
self.clients.append(self)
prontserve.listeners.add(self)
self.write_message({'headers': {
'jobs': prontserve.jobs.public_list(),
'continous_movement': False
}})
# Send events to initialize the machine's state
self.on_sensor_changed()
for k, v in prontserve.target_values.iteritems():
self.on_uncaught_event("target_temp_changed", {k: v})
self.on_uncaught_event("job_progress_changed", prontserve.previous_job_progress)
# Alerting all websockets of the new client
self._on_connections_changed()
self.send(initialized= {'session_uuid': self.session_uuid} )
print "WebSocket opened. %i sockets currently open." % len(prontserve.listeners)
def _on_connections_changed(self):
for c in self.clients:
c.send(connections_changed= len(prontserve.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):
if not self.authorized: return
cmds = self._cmds()
print "message received: %s"%(msg)
msg = re.sub(r'\s+', "\s" ,msg).strip()
msg = msg.replace(":\s", ":")
msg = msg.replace("@\s", "@")
msg = msg.replace("@", "at:")
words = msg.split("\s")
cmd = words[0]
arg_words = words[1:]
args = []
kwargs = {}
for w in arg_words:
if len(w) == 0: continue
if w.find(":") > -1:
k, v = w.split(":")
kwargs[k] = v
else:
args.append(w)
if cmd in cmds.keys():
try:
self._throwArgErrors(cmd, args, kwargs)
except Exception as ex:
self._error(message=str(ex), type= 'bad_cmd')
try:
# Set is already used by pronsole so we use construct_set
if cmd == "set": cmd = "construct_set"
# Run the command
response = getattr(prontserve, "do_%s"%cmd)(*args, **kwargs)
self.write_message({"ack": response})
except Exception as ex:
print traceback.format_exc()
self._error(message= str(ex), type= 'machine_error')
else:
self._error(message= "%s command does not exist."%cmd, type= 'bad_cmd')
def _error(self, **kwargs):
self.write_message({"error": kwargs})
def _cmds(self):
return {
"home": {
'array_args': True,
'args_error': "Home only (optionally) accepts axe names."
},
"move": {
'named_args': True,
'args_error': textwrap.dedent("""
Move only takes a list of axes, distance pairs and optionally @
prefixed feedrates.
""").strip()
},
"set": {
'namespaced': True, # namespaced by the machine aspect (temp/motors/fan)
'named_args': True,
'array_args': True,
'args_error': textwrap.dedent("""
Set only accepts a namespace (temp, motors or fan) and a list of
heater, value pairs for the temp or on or off for the motor and fan
namespaces.
""").strip()
},
"estop": {
'args_error': "Estop does not accept any parameters."
},
"print": {
'args_error': "Print does not accept any parameters."
},
"rm_job": {
'namespaced': True, # namespaced by the job id
'args_error': "Rm_job only accepts a job_id."
},
"change_job": {
'namespaced': True, # namespaced by the job id
'named_args': True,
'args_error': textwrap.dedent("""
Change_job only accepts a job_id and a list of key/value pairs.
""").strip()
},
"get_jobs": {
'args_error': "Get_jobs does not accept any parameters."
}
}
def _throwArgErrors(self, cmd, args, kwargs):
meta = self._cmds()[cmd]
arg_errors = (len(kwargs) > 0 and not 'named_args' in meta)
arg_errors = arg_errors or (len(args) == 0 and 'namespaced' in meta)
minArgs = 0
if 'namespaced' in meta: minArgs = 1
arg_errors = arg_errors or (len(args) > minArgs and not 'array_args' in meta)
if arg_errors: raise Exception(meta['args_error'])
def on_close(self):
if self in self.clients:
self.clients.remove(self)
prontserve.listeners.remove(self)
self._on_connections_changed()
print "WebSocket closed. %i sockets currently open." % len(prontserve.listeners)
dir = os.path.dirname(__file__)
settings = dict(
template_path=os.path.join(dir, "printrun", "server", "templates"),
static_path=os.path.join(dir, "printrun", "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
# Faster GCoder implementation without any parsing # Faster GCoder implementation without any parsing
# ------------------------------------------------- # -------------------------------------------------
...@@ -442,34 +63,60 @@ class Prontserve(pronsole.pronsole, EventEmitter): ...@@ -442,34 +63,60 @@ class Prontserve(pronsole.pronsole, EventEmitter):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.initializing = True self.initializing = True
self.max_w_val = 0
pronsole.pronsole.__init__(self) pronsole.pronsole.__init__(self)
EventEmitter.__init__(self) EventEmitter.__init__(self)
self.reset_timeout = 0
self.settings.sensor_names = {'T': 'extruder', 'B': 'bed'} self.settings.sensor_names = {'T': 'extruder', 'B': 'bed'}
self.settings.sensor_poll_rate = 1 # seconds
self.p.loud = kwargs['loud'] self.p.loud = kwargs['loud']
self.dry_run = kwargs['dry_run'] == True self.dry_run = kwargs['dry_run'] == True
self.stdout = sys.stdout self.stdout = sys.stdout
self.ioloop = tornado.ioloop.IOLoop.instance()
self.settings.sensor_poll_rate = 1 # seconds
self.sensors = {'extruder': -1, 'bed': -1}
self.target_values = {'e0': 0, 'b': 0}
self.fan_speed = 255
self.fan_enabled = False
self.load_default_rc() 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
self._sensor_update_received = True
self.max_w_val = 0
self.waiting_to_reach_temp = False
self.p.sendcb = self.sendcb self.p.sendcb = self.sendcb
self.jobs.listeners.add(self)
self.initializing = False self.initializing = False
dir = os.path.dirname(__file__)
self.server = ConstructServer(
printer= self,
settings= self.settings,
components= dict(
temps= ["e0", "b"],
fans= ["f0"],
conveyors= ["c0"],
axes= ["x", "y", "z"]
),
server_settings= dict(
template_path= os.path.join(dir, "printrun", "server", "templates"),
static_path= os.path.join(dir, "printrun", "server", "static"),
debug= True
),
routes= [
(r"/", RootHandler),
(r"/inspect", InspectHandler)
]
)
def display_startup_padding(self):
if self.dry_run:
for i in range(0,7):
sys.stdout.write("\x1B[0;33m Dry Run \x1B[0m")
print ""
def display_startup_message(self):
welcome = textwrap.dedent(u"""
+---+ \x1B[0;32mProntserve: Your printer just got a whole lot better.
\x1B[0m| \u2713 | Ready to print.
+---+ More details at http://localhost:8888/""")
print "\n"+"-"*80
self.display_startup_padding()
sys.stdout.write(welcome)
print "\n"
self.display_startup_padding()
print "-"*80 + "\n"
def start(self): def start(self):
try:
# Connect to the printer
if self.dry_run == False: if self.dry_run == False:
self.do_connect("") self.do_connect("")
if self.p.printer == None: sys.exit(1) if self.p.printer == None: sys.exit(1)
...@@ -483,37 +130,38 @@ class Prontserve(pronsole.pronsole, EventEmitter): ...@@ -483,37 +130,38 @@ class Prontserve(pronsole.pronsole, EventEmitter):
if self.p.online == False: if self.p.online == False:
print "Unable to connect to printer: Connection timed-out." print "Unable to connect to printer: Connection timed-out."
sys.exit(1) sys.exit(1)
# Wait for the printer to finish connecting and then reset it
time.sleep(2) time.sleep(2)
self.reset() self.reset()
# Start the server, display the startup message and start the ioloop
self.server.start()
self.display_startup_message()
self.server.ioloop.start()
except Exception as ex:
print traceback.format_exc()
if args.heaptrace: print hpy().heap()
self.p.disconnect()
exit()
tornado.ioloop.PeriodicCallback( def is_online(self):
self.run_print_queue_loop, 300, self.ioloop return self.p.online == True
).start()
tornado.ioloop.PeriodicCallback(
self.run_sensor_loop, self.settings.sensor_poll_rate*1000, self.ioloop
).start()
# Initialize DNS-SD once the server is ready to go online
self.init_dns_sd()
def is_printing(self):
return self.p.printing == False and self.p.online
def init_dns_sd(self): def post_process_print_job(self, filename, filebody):
sdRef = pybonjour.DNSServiceRegister(name = None, return FastGCode(filebody.split("\n"))
regtype = '_construct._tcp',
port = 8888,
domain = "local.")
atexit.register(self.cleanup_service, sdRef)
def cleanup_service(self, sdRef): def print_progress(self):
sdRef.close() if(self.p.printing):
return 100*float(self.p.queueindex)/len(self.p.mainqueue)
if(self.sdprinting):
return self.percentdone
return 0
def do_print(self): def start_print_job(self, job):
if not self.p.online: raise Exception("Not online") self.p.startprint(job['body'])
if self.printing_jobs: raise Exception("Already printing") self.p.paused = False
no_jobs_msg = "Nothing to print. Try adding a print job with add_job."
if len(self.jobs.list) == 0: raise Exception(no_jobs_msg)
self.printing_jobs = True
def do_home(self, *args, **kwargs): def do_home(self, *args, **kwargs):
pronsole.pronsole.do_home(self, " ".join(args)) pronsole.pronsole.do_home(self, " ".join(args))
...@@ -546,249 +194,120 @@ class Prontserve(pronsole.pronsole, EventEmitter): ...@@ -546,249 +194,120 @@ class Prontserve(pronsole.pronsole, EventEmitter):
def do_estop(self): def do_estop(self):
self.reset() self.reset()
print "Emergency Stop!" print "Emergency Stop!"
self.fire("estop")
# Updating the job progress
print self.print_progress()
self.update_job_progress(self.print_progress())
for k, v in self.target_values.iteritems():
self._set_target_temp(k, 0)
self.do_set_motors("off")
self.do_set_fan("off")
progress = {'eta': 0, 'percent': 100}
self.fire("target_temp_progress_changed", {'e0': progress})
# Not thread safe; must be run from the ioloop thread.
def reset(self): def reset(self):
self.printing_jobs = False self.async(self.server.set_waiting_to_reach_temp, None)
self.current_job = None
self.waiting_to_reach_temp = False
# pause the print job if any is printing # pause the print job if any is printing
if self.p.printing: if self.p.printing:
pronsole.pronsole.do_pause(self, "") pronsole.pronsole.do_pause(self, "")
# Prevent the sensor from polling for 2 seconds while the firmware # Prevent the sensor from polling for 2 seconds while the firmware
# restarts # restarts
self.reset_timeout = time.time() + 2 self.server.set_reset_timeout(time.time() + 2)
self._sensor_update_received = True self.server.set_sensor_update_received(True)
# restart the firmware # restart the firmware
pronsole.pronsole.do_reset(self, "") pronsole.pronsole.do_reset(self, "")
self.p.printing = False self.p.printing = False
def on_target_temp_changed(self, target=None, value=None):
gcode = "M104"
if target == "b": gcode = "M140"
if not target in ["b", "e0"]: gcode += " p%i"%(int(target[1:]))
gcode += " S%f"%float(value)
self.p.send_now(gcode)
def on_fan_enabled_changed(self, target=None, value=None):
update_fan()
def do_construct_set(self, subCmd, *args, **kwargs): def on_fan_speed_changed(self, target=None, value=None):
method = "do_set_%s"%subCmd update_fan()
if not hasattr(self, method):
raise Exception("%s is not a real namespace"%subCmd)
getattr(self, method)(*args, **kwargs)
def do_set_temp(self, **kwargs):
# Setting each temperature individually
prefixes = {'b': 'bed', 'e0': 'set', 'e': 'set'}
for k, prefix in prefixes.iteritems():
if not k in kwargs: continue
print "%stemp %s"%(prefix, kwargs[k])
setter = getattr(pronsole.pronsole, "do_%stemp"%prefix)
setter(self, kwargs[k])
pprint({prefix: kwargs[k]})
def do_set_fan(self, *args, **kwargs):
states = {"on": True, "off": False}
if len(args) == 1 and len(kwargs) == 0 and (args[0] in states):
self.fan_enabled = states[args[0].lower()]
self.fire("fan_enabled_changed", self.fan_enabled)
elif "speed" in kwargs:
try:
self.fan_speed = float(kwargs["speed"])
except:
raise Exception("Fan speed must be a number")
self.fire("fan_speed_changed", self.fan_speed)
else:
raise Exception("""
Bad set fan parameters. Please either use `set fan speed: [NUMBER]`
or `set fan [ON|OFF]`.
""")
def update_fan(self):
if self.fan_enabled == False: if self.fan_enabled == False:
speed = 0 speed = 0
else: else:
speed = int(self.fan_speed) speed = int(self.server.c_get("f0", "fan_speed"))
print "M106 S%i"%speed print "M106 S%i"%speed
self.p.send_now("M106 S%i"%speed) self.p.send_now("M106 S%i"%speed)
def on_motors_enabled_changed(self, target=None, value=None):
def do_set_motors(self, *args):
value = {"on": True, "off": False}[args[0].lower()]
self.p.send_now({True: "M17", False: "M18"}[value]) self.p.send_now({True: "M17", False: "M18"}[value])
self.fire("motors_enabled_changed", value)
def do_set_feedrate(self, **kwargs):
# TODO: kwargs[xy] * 60 and kwargs[z] * 60
pass
def do_add_job(self, filename, filebody):
self.jobs.add(filename, filebody)
def do_rm_job(self, job_id):
self.jobs.remove(int(job_id))
def do_change_job(self, job_id, **kwargs):
print job_id
print kwargs
try:
job_id = int(job_id)
except:
raise Exception("job_id must be a number")
self.jobs.update(job_id, kwargs)
def do_get_jobs(self):
jobexport = []
if self.current_job != None:
jobexport.append(
dict(
id = self.current_job["id"],
file_name = self.current_job["file_name"],
printing = True
)
)
jobexport.extend(self.jobs.public_list())
return {'jobs': jobexport}
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
try:
if self.printing_jobs and p.printing == 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 self.settings.pause_between_prints and self.current_job != None:
print "Print job complete. Pausing between jobs."
self.current_job = None
self.printing_jobs = False
elif len(self.jobs.list) > 0:
print "Starting the next print job"
self.current_job = self.jobs.list.pop(0)
lines = self.current_job['body'].split("\n")
gc = FastGCode(lines)
self.p.startprint(gc)
self.p.paused = False
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 def request_sensor_update(self):
self.update_job_progress(self.print_progress())
except Exception as ex:
print traceback.format_exc()
def update_job_progress(self, progress):
if progress != self.previous_job_progress:
self.previous_job_progress = progress
self.fire("job_progress_changed", progress)
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 run_sensor_loop(self):
# A number of conditions that must be met for us to send a temperature
# request to the printer. This safeguards this printer from being overloaded
# by temperature requests it cannot presently respond to.
ready = self._sensor_update_received
ready = ready and (time.time() - self.reset_timeout) > 0
ready = ready and (not self.waiting_to_reach_temp)
if not ready: return
self._sensor_update_received = False
if self.dry_run: if self.dry_run:
self._receive_sensor_update( return self._receive_sensor_update(
"ok T:%i B:%i"%(random.randint(30, 60), random.randint(30, 60)) "ok T:%i B:%i"%(random.randint(30, 60), random.randint(30, 60))
) )
else:
self.request_sensor_update()
def request_sensor_update(self):
if self.p.online: self.p.send_now("M105") if self.p.online: self.p.send_now("M105")
def recvcb(self, l): def recvcb(self, l):
""" Parses a line of output from the printer via printcore """ """ Parses a line of output from the printer via printcore """
l = l.rstrip() l = l.rstrip()
#print l #print l
if self.waiting_to_reach_temp and ("ok" in l): if self.server.waiting_to_reach_temp and ("ok" in l):
self.waiting_to_reach_temp = False self.async(self.server.set_waiting_to_reach_temp, None)
if ("T:" in l): if ("T:" in l):
self.ioloop.add_callback(self._receive_sensor_update, l) self.async(self._receive_sensor_update, l)
if l!="ok" and not l.startswith("ok T") and not l.startswith("T:"): if l!="ok" and not l.startswith("ok T") and not l.startswith("T:"):
self.ioloop.add_callback(self._receive_printer_error, l) self.async(self._receive_printer_error, l)
def sendcb(self, l): def sendcb(self, l):
# Monitor the sent commands for new extruder target temperatures # Monitor the sent commands for new extruder target temperatures
if ("M109" in l) or ("M104" in l) or ("M140" in l) or ("M190" in l): if ("M109" in l) or ("M104" in l) or ("M140" in l) or ("M190" in l):
temp = float(re.search('S([0-9]+)', l).group(1)) temp = float(re.search('S([0-9]+)', l).group(1))
if ("M109" in l) or ("M104" in l): if ("M109" in l) or ("M104" in l):
self.ioloop.add_callback(self._set_target_temp, "e0", temp) target = "e0"
if " P" in l: target = "e%i"%(int(re.search(' P([0-9]+)', l).group(1)))
else: else:
self.ioloop.add_callback(self._set_target_temp, "b", temp) target = "b"
self.async(self.server.c_set, target, "target_temp", temp, internal=True)
if ("M109" in l) or ("M190" in l) or ("M116" in l): if ("M109" in l) or ("M190" in l) or ("M116" in l):
self.waiting_to_reach_temp = True if ("M116" in l): target = "e0"
self.async(self.server.set_waiting_to_reach_temp, [target])
def _set_target_temp(self, key, temp): # Adds a callback to the ioloop to run a method later on in the server thread
self.target_values[key] = temp def async(self, *args, **kwargs):
self.fire("target_temp_changed", {key: temp}) self.server.ioloop.add_callback(*args, **kwargs)
def _receive_sensor_update(self, l): def _receive_sensor_update(self, l):
try: try:
self._sensor_update_received = True self.async(self.server.set_sensor_update_received, True)
words = filter(lambda s: s.find(":") > 0, l.split(" ")) words = filter(lambda s: s.find(":") > 0, l.lower().split(" "))
d = dict([ s.split(":") for s in words]) d = dict([ s.split(":") for s in words])
for key, value in d.iteritems(): for key, value in d.iteritems():
self.__update_sensor(key, value) if key == "t": key = "e0"
if not key in self.server.components: continue
self.fire("sensor_changed") self.server.c_set(key, "current_temp", float(value), internal=True)
# Fire a event if the extruder is giving us a countdown till it's online # Fire a event if the extruder is giving us a countdown till it's online
# see: TEMP_RESIDENCY_TIME (via the googles) # see: TEMP_RESIDENCY_TIME (via the googles)
# see: https://github.com/ErikZalm/Marlin/blob/Marlin_v1/Marlin/Marlin_main.cpp#L1191 # see: https://github.com/ErikZalm/Marlin/blob/Marlin_v1/Marlin/Marlin_main.cpp#L1191
if ("W" in d): if ("w" in d):
percent = 0 percent = 0
if not d["W"] == "?": if not d["w"] == "?":
w_val = float(d["W"]) w_val = float(d["w"])
if w_val > self.max_w_val: self.max_w_val = w_val if w_val > self.max_w_val: self.max_w_val = w_val
percent = (100 - w_val*100/self.max_w_val) percent = (100 - w_val*100/self.max_w_val)
progress = {'eta': w_val, 'percent': percent} progress = {'eta': w_val, 'percent': percent}
else: else:
progress = {'percent': percent} progress = {'percent': percent}
self.fire("target_temp_progress_changed", {'e0': progress}) for target in (self.server.waiting_to_reach_temp):
self.server.c_set(
target, "target_temp_progress", progress, internal=True
)
except Exception as ex: except Exception as ex:
print traceback.format_exc() print traceback.format_exc()
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): def log(self, *msg):
msg = ''.join(str(i) for i in msg) msg = ''.join(str(i) for i in msg)
msg.replace("\r", "") msg.replace("\r", "")
print msg print msg
self.fire("log", {'msg': msg, 'level': "debug"}) self.server.broadcast([
dict(type= "log", data= dict(msg= msg, level= "debug"))
])
def logError(self, *msg): def logError(self, *msg):
print u"".join(unicode(i) for i in msg) print u"".join(unicode(i) for i in msg)
...@@ -802,80 +321,6 @@ class Prontserve(pronsole.pronsole, EventEmitter): ...@@ -802,80 +321,6 @@ class Prontserve(pronsole.pronsole, EventEmitter):
True True
class PrintJobQueue(EventEmitter):
def __init__(self):
super(PrintJobQueue, self).__init__()
self.list = []
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"],
file_name = job["file_name"],
printing = False,
)
def add(self, file_name, body):
ext = os.path.splitext(file_name)[1]
job = dict(
id = self.__last_id,
file_name=file_name,
body= body,
)
self.__last_id += 1
self.list.append(job)
print "Added %s"%(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['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:
raise Exception("There is no job #%i."%job_id)
# proposed future print quantity functionality
# if hasattr(job_attrs, 'qty'): job['qty'] = qty
if job_attrs['position']:
position = int(job_attrs['position'])
self.list.remove(job)
self.list.insert(position, job)
print int(job_attrs['position'])
print "Print #%s Job Updated ( %s )."%(job['id'], job['file_name'])
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 # Server Start Up
# ------------------------------------------------- # -------------------------------------------------
...@@ -898,35 +343,5 @@ if __name__ == "__main__": ...@@ -898,35 +343,5 @@ if __name__ == "__main__":
) )
args = parser.parse_args() args = parser.parse_args()
dry_run = args.dry_run if args.heaptrace: from guppy import hpy
prontserve = Prontserve(dry_run=args.dry_run, loud=args.loud).start()
if args.heaptrace:
from guppy import hpy
def warn_if_dry_run():
if dry_run:
for i in range(0,7):
sys.stdout.write("\x1B[0;33m Dry Run \x1B[0m")
print ""
prontserve = Prontserve(dry_run=dry_run, loud=args.loud)
try:
prontserve.start()
application.listen(8888)
print "\n"+"-"*80
welcome = textwrap.dedent(u"""
+---+ \x1B[0;32mProntserve: Your printer just got a whole lot better.\x1B[0m
| \u2713 | Ready to print.
+---+ More details at http://localhost:8888/""")
warn_if_dry_run()
sys.stdout.write(welcome)
print "\n"
warn_if_dry_run()
print "-"*80 + "\n"
prontserve.ioloop.start()
except:
if args.heaptrace: print hpy().heap()
prontserve.p.disconnect()
exit()
inflection
pybonjour pybonjour
git+https://github.com/D1plo1d/tornado.git git+https://github.com/D1plo1d/tornado.git
git+https://github.com/D1plo1d/py-mdns.git git+https://github.com/D1plo1d/py-mdns.git
git+https://github.com/construct-protocol/construct_server.py
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment