def write(self, data): log.debug("-> Test | {}".format(data)) #: Python 3 is annoying if hasattr(data, 'encode'): data = data.encode() self.buffer.write(data)
def disconnect(self): """ Disconnect from the device. By default this delegates handling to the active transport or connection handler. """ log.debug("device | disconnect") cmd = self.config.commands_disconnect if cmd: yield defer.maybeDeferred(self.connection.write, cmd) yield defer.maybeDeferred(self.connection.disconnect)
def data_received(self, data): """ Called when the device replies back with data. This can occur at any time as communication is asynchronous. The protocol should handle as needed. Parameters ---------- data """ log.debug("data received: {}".format(data))
def save_area(self): """ Save the dock area for the workspace. """ log.debug("Saving dock area") area = self.content.find('dock_area') try: with open('inkcut.workspace.db', 'w') as f: f.write(pickle.dumps(area)) except Exception as e: log.warning("Error saving dock area: {}".format(e)) return e
def order(self, job, path): """ Sort subpaths by minimizing the distances between all start and end points. """ subpaths = split_painter_path(path) log.debug("Subpath count: {}".format(len(subpaths))) # Cache all start and end points now = time() # This is in the UI thread time_limit = now + self.plugin.optimizer_timeout zero = QVector2D(0, 0) for sp in subpaths: # Average start and end into one "vertex" start = sp.elementAt(0) end = sp.elementAt(sp.elementCount() - 1) sp.start_point = QVector2D(start.x, start.y) sp.end_point = QVector2D(end.x, end.y) distance = QVector2D.distanceToPoint original = subpaths[:] result = [] p = zero while subpaths: best = sys.maxsize shortest = None for sp in subpaths: d = distance(p, sp.start_point) if d < best: best = d shortest = sp p = shortest.end_point result.append(shortest) subpaths.remove(shortest) # time.time() is slow so limit the calls if time() > time_limit: result.extend(subpaths) # At least part of it is optimized log.warning( "Shortest path search aborted (time limit reached)") break duration = time() - now d = self.subpath_move_distance(zero, original) d = d - self.subpath_move_distance(zero, result) log.debug("Shortest path search: Saved {} in of movement in {}".format( to_unit(d, 'in'), duration)) return join_painter_paths(result)
def _refresh_commands(self, change=None): """ Reload all CliCommands registered by any Plugins Any plugin can add to this list by providing a CliCommand extension in the PluginManifest. If the system arguments match the command it will be invoked with the given arguments as soon as the plugin that registered the command is loaded. Thus you can effectively hook your cli argument at any stage of the process. """ workbench = self.workbench point = workbench.get_extension_point(extensions.CLI_COMMAND_POINT) commands = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for d in extension.get_children(extensions.CliCommand): commands.append( Command( declaration=d, workbench=self.workbench, )) #: Update self.commands = commands #: Log that they've been updated log.debug("CLI | Commands loaded") #: Recreate the parser self.parser = self._default_parser() #: Parse the args, if an error occurs the program will exit #: but if no args are given it continues try: args = self.parser.parse_args() except ArgumentError as e: #: Ignore errors that may occur because commands havent loaded yet if [m for m in ['invalid choice'] if m in str(e.message)]: return self.parser.exit_with_error(e.message) #: Run it but defer it until the next available loop so the current #: plugin finishes loading if hasattr(args, 'cmd'): self.workbench.application.deferred_call(self.run, args.cmd, args) else: log.debug("CLI | No cli command was given.")
def _refresh_commands(self, change=None): """ Reload all CliCommands registered by any Plugins Any plugin can add to this list by providing a CliCommand extension in the PluginManifest. If the system arguments match the command it will be invoked with the given arguments as soon as the plugin that registered the command is loaded. Thus you can effectively hook your cli argument at any stage of the process. """ workbench = self.workbench point = workbench.get_extension_point(extensions.CLI_COMMAND_POINT) commands = [] for extension in sorted(point.extensions, key=lambda ext: ext.rank): for d in extension.get_children(extensions.CliCommand): commands.append(Command( declaration=d, workbench=self.workbench, )) #: Update self.commands = commands #: Log that they've been updated log.debug("CLI | Commands loaded") #: Recreate the parser self.parser = self._default_parser() #: Parse the args, if an error occurs the program will exit #: but if no args are given it continues try: args = self.parser.parse_args() except ArgumentError as e: #: Ignore errors that may occur because commands havent loaded yet if [m for m in ['invalid choice'] if m in str(e.message)]: return self.parser.exit_with_error(e.message) #: Run it but defer it until the next available loop so the current #: plugin finishes loading if hasattr(args, 'cmd'): self.workbench.application.deferred_call(self.run, args.cmd, args) else: log.debug("CLI | No cli command was given.")
def connect(self): """ Connect to the device. By default this delegates handling to the active transport or connection handler. Returns ------- result: Deferred or None May return a Deferred object that the process will wait for completion before continuing. """ log.debug("device | connect") yield defer.maybeDeferred(self.connection.connect) cmd = self.config.commands_connect if cmd: yield defer.maybeDeferred(self.connection.write, cmd)
def run(self, cmd, args): """ Run the given command with the given arguments. """ #: If a sub command was given in the cli, invoke it. try: cmd = args.cmd log.debug("CLI | Runinng command '{}' with args: {}".format( cmd.declaration.name, args)) sys.exit(cmd.run(args)) except extensions.StopSystemExit: pass except Exception as e: #: Catch and exit, if we don't do this it will open the #: startup error dialog. log.error(traceback.format_exc()) sys.exit(-1)
def init(self, job): """ Initialize the job. This should do any final path manipulation required by the device (or as specified by the config) and any filters should be applied here (overcut, blade offset compensation, etc..). The connection is not active at this stage. Parameters ----------- job: inkcut.job.models.Job instance The job to handle. Returns -------- model: QtGui.QPainterPath instance or Deferred that resolves to a QPainterPath if heavy processing is needed. This path is then interpolated and sent to the device. """ log.debug("device | init {}".format(job)) config = self.config # Set the speed of this device for tracking purposes units = config.speed_units.split("/")[0] job.info.speed = from_unit(config.speed, units) scale = config.scale[:] if config.mirror_x: scale[0] *= -1 if config.mirror_x else 1 if config.mirror_y: scale[1] *= -1 if config.mirror_y else 1 # Get the internal QPainterPath "model" transformed to how this # device outputs model = job.create(swap_xy=config.swap_xy, scale=scale) if job.feed_to_end: #: Move the job to the new origin x, y, z = self.origin model.translate(x, -y) #: TODO: Apply filters here #: Return the transformed model return model
def _default_optimized_path(self): """ Filter parts of the documen based on the selected layers and colors """ doc = self.path for f in self.filters: # If the color/layer is NOT enabled, then remove that color/layer if not f.enabled: log.debug("Applying filter {}".format(f)) doc = f.apply_filter(self, doc) # Apply ordering to path # this delegates to objects in the ordering module OrderingHandler = ordering.REGISTRY.get(self.order) if OrderingHandler: doc = OrderingHandler().order(self, doc) return doc
def init(self, job): """ Initialize the job. This should do any final path manipulation required by the device (or as specified by the config) and any filters should be applied here (overcut, blade offset compensation, etc..). The connection is not active at this stage. Parameters ----------- job: inkcut.job.models.Job instance The job to handle. Returns -------- model: QtGui.QPainterPath instance or Deferred that resolves to a QPainterPath if heavy processing is needed. This path is then interpolated and sent to the device. """ log.debug("device | init {}".format(job)) config = self.config #: Set the speed of this device for tracking purposes units = config.speed_units.split("/")[0] job.info.speed = from_unit(config.speed, units) #: Get the internal QPainterPath "model" model = job.model #: Transform the path to the device coordinates model = self.transform(model) if job.feed_to_end: #: Move the job to the new origin x, y, z = self.origin model.translate(x, -y) #: TODO: Apply filters here #: Return the transformed model return model
def order(self, job, path): """ Sort subpaths by minimizing the distances between all start and end points. """ subpaths = split_painter_path(path) log.debug("Subpath count: {}".format(len(subpaths))) # Cache all start and end points time_limit = time()+self.time_limit zero = QVector2D(0, 0) for sp in subpaths: # Average start and end into one "vertex" start = sp.elementAt(0) end = sp.elementAt(sp.elementCount()-1) sp.start_point = QVector2D(start.x, start.y) sp.end_point = QVector2D(end.x, end.y) distance = QVector2D.distanceToPoint original = subpaths[:] result = [] p = zero while subpaths: best = sys.maxsize shortest = None for sp in subpaths: d = distance(p, sp.start_point) if d < best: best = d shortest = sp p = shortest.end_point result.append(shortest) subpaths.remove(shortest) # time.time() is slow so limit the calls if time() > time_limit: result.extend(subpaths) # At least part of it is optimized log.debug("Shortest path search aborted (time limit reached)") break d = self.subpath_move_distance(zero, original) d = d-self.subpath_move_distance(zero, result) log.debug("Shortest path search: Saved {} in of movement ".format( to_unit(d, 'in'))) return join_painter_paths(result)
def set_force(self, f): log.debug("protocol.set_force({f})".format(f=f))
def set_pen(self, p): log.debug("protocol.set_pen({p})".format(p=p))
def set_velocity(self, v): log.debug("protocol.set_velocity({v})".format(v=v))
def connection_made(self): log.debug("protocol.connectionMade()")
def move(self, x, y, z, absolute=True): log.debug("protocol.move({x},{y},{z})".format(x=x, y=y, z=z)) #: Wait some time before we get there return async_sleep(0.1)
def process(self, model): """ Process the path model of a job and return each command within the job. Parameters ---------- model: QPainterPath The path to process Returns ------- generator: A list or generator object that yields each command to invoke on the device and the distance moved. In the format (distance, cmd, args, kwargs) """ config = self.config #: Previous point _p = QtCore.QPointF(self.origin[0], self.origin[1]) #: Do a final translation since Qt's y axis is reversed t = QtGui.QTransform.fromScale(1, -1) model = model * t #: Determine if interpolation should be used skip_interpolation = (self.connection.always_spools or config.spooled or not config.interpolate) # speed = distance/seconds # So distance/speed = seconds to wait step_size = config.step_size if not skip_interpolation and step_size <= 0: raise ValueError("Cannot have a step size <= 0!") try: # Apply device filters for f in self.filters: log.debug(" filter | Running {} on model".format(f)) model = f.apply_to_model(model) # Some versions of Qt seem to require a value in toSubpathPolygons m = QtGui.QTransform.fromScale(1, 1) polypath = model.toSubpathPolygons(m) # Apply device filters to polypath for f in self.filters: log.debug(" filter | Running {} on polypath".format(f)) polypath = f.apply_to_polypath(polypath) for path in polypath: #: And then each point within the path #: this is a polygon for i, p in enumerate(path): #: Head state # 0 move, 1 cut z = 0 if i == 0 else 1 #: Make a subpath subpath = QtGui.QPainterPath() subpath.moveTo(_p) subpath.lineTo(p) #: Update the last point _p = p #: Total length l = subpath.length() #: If the device does not support streaming #: the path interpolation is skipped entirely if skip_interpolation: x, y = p.x(), p.y() yield (l, self.move, ([x, y, z], ), {}) continue #: Where we are within the subpath d = 0 #: Interpolate path in steps of dl and ensure we get #: _p and p (t=0 and t=1) #: This allows us to cancel mid point while d <= l: #: Now set d to the next point by step_size #: if the end of the path is less than the step size #: use the minimum of the two dl = min(l - d, step_size) #: Now find the point at the given step size #: the first point d=0 so t=0, the last point d=l so t=1 t = subpath.percentAtLength(d) sp = subpath.pointAtPercent(t) #if d == l: # break #: Um don't we want to send the last point?? #: -y because Qt's axis is from top to bottom not bottom #: to top x, y = sp.x(), sp.y() yield (dl, self.move, ([x, y, z], ), {}) #: When we reached the end but instead of breaking above #: with a d < l we do it here to ensure we get the last #: point if d == l: #: We reached the end break #: Add step size d += dl #: Make sure we get the endpoint ep = model.currentPosition() yield (0, self.move, ([ep.x(), ep.y(), 0], ), {}) except Exception as e: log.error("device | processing error: {}".format( traceback.format_exc())) raise e
def move(self, position, absolute=True): """ Move to position. Based on this publication http://goldberg.berkeley.edu/pubs/XY-Interpolation-Algorithms.pdf Parameters ---------- dx: int steps in x direction or x position dy: int steps in y direction or y position absolute: boolean if true move to absolute position, else move relative to current position """ dx, dy, z = position #: Local refs are faster config = self.config dx, dy = int(dx * config.scale[0]), int(dy * config.scale[1]) _pos = self._position if absolute: dx -= _pos[0] dy -= _pos[1] if dx == dy == 0: log.info("{}, {}".format(_pos, _pos)) return sx = dx > 0 and 1 or -1 sy = dy > 0 and 1 or -1 fxy = abs(dx) - abs(dy) x, y = 0, 0 ax, ay = abs(dx), abs(dy) stepx, stepy = self.motor[0].step, self.motor[1].step log.info("{}, {}".format(dx, dy)) try: while True: if fxy < 0: fxy += ax stepy(sy) y += sy else: fxy -= ay stepx(sx) x += sx #: Wait for both movements to complete #yield DeferredList([stepx(mx), # stepy(my)]) # log.debug("x={} dx={}, y={} dy={}".format(x,dx,y,dy)) if x == dx and y == dy: self._position = [_pos[0] + dx, _pos[1] + dy, z] break except KeyboardInterrupt: self.disconnect() raise log.debug(self._position) self.position = position
def move(self, position, absolute=True): """ Move to position. Based on this publication http://goldberg.berkeley.edu/pubs/XY-Interpolation-Algorithms.pdf Parameters ---------- dx: int steps in x direction or x position dy: int steps in y direction or y position absolute: boolean if true move to absolute position, else move relative to current position """ dx, dy, z = position #: Local refs are faster config = self.config dx, dy = int(dx*config.scale[0]), int(dy*config.scale[1]) _pos = self._position if absolute: dx -= _pos[0] dy -= _pos[1] if dx == dy == 0: log.info("{}, {}".format(_pos, _pos)) return sx = dx > 0 and 1 or -1 sy = dy > 0 and 1 or -1 fxy = abs(dx)-abs(dy) x, y = 0, 0 ax, ay = abs(dx), abs(dy) stepx, stepy = self.motor[0].step, self.motor[1].step log.info("{}, {}".format(dx, dy)) try: while True: if fxy < 0: fxy += ax stepy(sy) y += sy else: fxy -= ay stepx(sx) x += sx #: Wait for both movements to complete #yield DeferredList([stepx(mx), # stepy(my)]) # log.debug("x={} dx={}, y={} dy={}".format(x,dx,y,dy)) if x == dx and y == dy: self._position = [_pos[0]+dx, _pos[1]+dy, z] break except KeyboardInterrupt: self.disconnect() raise log.debug(self._position) self.position = position
def submit(self, job, test=False): """ Submit the job to the device. If the device is currently running a job it will be queued and run when this is finished. This handles iteration over the path model defined by the job and sending commands to the actual device using roughly the procedure is as follows: device.connect() model = device.init(job) for cmd in device.process(model): device.handle(cmd) device.finish() device.disconnect() Subclasses provided by your own DeviceDriver may reimplement this to handle path interpolation however needed. The return value is ignored. The live plot view will update whenever the device.position object is updated. On devices with lower cpu/gpu capabilities this should be updated sparingly (ie the raspberry pi). Parameters ----------- job: Instance of `inkcut.job.models.Job` The job to execute on the device test: bool Do a test run. This specifies whether the commands should be sent to the actual device or not. If True, the connection will be replaced with a virtual connection that captures all the command output. """ log.debug("device | submit {}".format(job)) try: #: Only allow one job at a time if self.busy: queue = self.queue[:] queue.append(job) self.queue = queue #: Copy and reassign so the UI updates log.info("Job {} put in device queue".format(job)) return with self.device_busy(): #: Set the current the job self.job = job self.status = "Initializing job" #: Get the time to sleep based for each unit of movement config = self.config #: Rate px/ms if config.custom_rate >= 0: rate = config.custom_rate elif self.connection.always_spools or config.spooled: rate = 0 elif config.interpolate: if config.step_time > 0: rate = config.step_size / float(config.step_time) else: rate = 0 # Undefined else: rate = from_unit( config.speed, # in/s or cm/s config.speed_units.split("/")[0]) / 1000.0 # Device model is updated in real time model = yield defer.maybeDeferred(self.init, job) #: Local references are faster info = job.info #: Determine the length for tracking progress whole_path = QtGui.QPainterPath() #: Some versions of Qt seem to require a value in #: toSubpathPolygons m = QtGui.QTransform.fromScale(1, 1) for path in model.toSubpathPolygons(m): for i, p in enumerate(path): whole_path.lineTo(p) total_length = whole_path.length() total_moved = 0 log.debug("device | Path length: {}".format(total_length)) #: So a estimate of the duration can be determined info.length = total_length info.speed = rate * 1000 #: Convert to px/s #: Waiting for approval info.status = 'waiting' #: If marked for auto approve start now if info.auto_approve: info.status = 'approved' else: #: Check for approval before starting yield defer.maybeDeferred(info.request_approval) if info.status != 'approved': self.status = "Job cancelled" return #: Update stats info.status = 'running' info.started = datetime.now() self.status = "Connecting to device" with self.device_connection(test or config.test_mode) as connection: self.status = "Processing job" try: yield defer.maybeDeferred(self.connect) #: Write startup command if config.commands_before: yield defer.maybeDeferred(connection.write, config.commands_before) self.status = "Working..." #: For point in the path for (d, cmd, args, kwargs) in self.process(model): #: Check if we paused if info.paused: self.status = "Job paused" #: Sleep until resumed, cancelled, or the #: connection drops while (info.paused and not info.cancelled and connection.connected): yield async_sleep(300) # ms #: Check for cancel for non interpolated jobs if info.cancelled: self.status = "Job cancelled" info.status = 'cancelled' break elif not connection.connected: self.status = "connection error" info.status = 'error' break #: Invoke the command #: If you want to let the device handle more complex #: commands such as curves do it in process and handle yield defer.maybeDeferred(cmd, *args, **kwargs) total_moved += d #: d should be the device must move in px #: so wait a proportional amount of time for the device #: to catch up. This avoids buffer errors from dumping #: everything at once. #: Since sending is way faster than cutting #: we must delay (without blocking the UI) before #: sending the next command or the device's buffer #: quickly gets filled and crappy china piece cutters #: get all jacked up. If the transport sends to a spooled #: output (such as a printer) this can be set to 0 if rate > 0: # log.debug("d={}, delay={} t={}".format( # d, delay, d/delay # )) yield async_sleep(d / rate) #: TODO: Check if we need to update the ui #: Set the job progress based on how far we've gone if total_length > 0: info.progress = int( max( 0, min(100, 100 * total_moved / total_length))) if info.status != 'error': #: We're done, send any finalization commands yield defer.maybeDeferred(self.finish) #: Write finalize command if config.commands_after: yield defer.maybeDeferred(connection.write, config.commands_after) #: Update stats info.ended = datetime.now() #: If not cancelled or errored if info.status == 'running': info.done = True info.status = 'complete' except Exception as e: log.error(traceback.format_exc()) raise finally: if connection.connected: yield defer.maybeDeferred(self.disconnect) #: Set the origin if job.feed_to_end and job.info.status == 'complete': self.origin = self.position #: If the user didn't cancel, set the origin and #: Process any jobs that entered the queue while this was running if self.queue and not job.info.cancelled: queue = self.queue[:] job = queue.pop(0) #: Pull the first job off the queue log.info("Rescheduling {} from queue".format(job)) self.queue = queue #: Copy and reassign so the UI updates #: Call a minute later timed_call(60000, self.submit, job) except Exception as e: log.error(' device | Execution error {}'.format( traceback.format_exc())) raise
def finish(self): """ Finish the job applying any cleanup necessary. """ log.debug("device | finish") return self.connection.protocol.finish()
def process(self, model): """ Process the path model of a job and return each command within the job. Parameters ---------- model: QPainterPath The path to process Returns ------- generator: A list or generator object that yields each command to invoke on the device and the distance moved. In the format (distance, cmd, args, kwargs) """ config = self.config # Previous point _p = QtCore.QPointF(self.origin[0], self.origin[1]) # Do a final translation since Qt's y axis is reversed from svg's # It should now be a bbox of (x=0, y=0, width, height) # this creates a copy model = model * QtGui.QTransform.fromScale(1, -1) # Determine if interpolation should be used skip_interpolation = (self.connection.always_spools or config.spooled or not config.interpolate) # speed = distance/seconds # So distance/speed = seconds to wait step_size = config.step_size if not skip_interpolation and step_size <= 0: raise ValueError("Cannot have a step size <= 0!") try: # Apply device filters for f in self.filters: log.debug(" filter | Running {} on model".format(f)) model = f.apply_to_model(model, job=self) # Since Qt's toSubpathPolygons converts curves without accepting # a parameter to set the minimum distance between points on the # curve, we need to prescale by a "quality factor" before # converting then undo the scaling to effectively adjust the # number of points on a curve. m = QtGui.QTransform.fromScale(config.quality_factor, config.quality_factor) # Some versions of Qt seem to require a value in toSubpathPolygons polypath = model.toSubpathPolygons(m) if config.quality_factor != 1: # Undo the prescaling, if the quality_factor > 1 the curve # quality will be improved. m_inv = QtGui.QTransform.fromScale(1 / config.quality_factor, 1 / config.quality_factor) polypath = list(map(m_inv.map, polypath)) # Apply device filters to polypath for f in self.filters: log.debug(" filter | Running {} on polypath".format(f)) polypath = f.apply_to_polypath(polypath) for path in polypath: #: And then each point within the path #: this is a polygon for i, p in enumerate(path): #: Head state # 0 move, 1 cut z = 0 if i == 0 else 1 #: Make a subpath subpath = QtGui.QPainterPath() subpath.moveTo(_p) subpath.lineTo(p) #: Update the last point _p = p #: Total length l = subpath.length() #: If the device does not support streaming #: the path interpolation is skipped entirely if skip_interpolation: x, y = p.x(), p.y() yield (l, self.move, ([x, y, z], ), {}) continue #: Where we are within the subpath d = 0 #: Interpolate path in steps of dl and ensure we get #: _p and p (t=0 and t=1) #: This allows us to cancel mid point while d <= l: #: Now set d to the next point by step_size #: if the end of the path is less than the step size #: use the minimum of the two dl = min(l - d, step_size) #: Now find the point at the given step size #: the first point d=0 so t=0, the last point d=l so t=1 t = subpath.percentAtLength(d) sp = subpath.pointAtPercent(t) #if d == l: # break #: Um don't we want to send the last point?? x, y = sp.x(), sp.y() yield (dl, self.move, ([x, y, z], ), {}) #: When we reached the end but instead of breaking above #: with a d < l we do it here to ensure we get the last #: point if d == l: #: We reached the end break #: Add step size d += dl #: Make sure we get the endpoint ep = model.currentPosition() x, y = ep.x(), ep.y() yield (0, self.move, ([x, y, 0], ), {}) except Exception as e: log.error("device | processing error: {}".format( traceback.format_exc())) raise e
def data_received(self, data): log.debug("protocol.data_received({}".format(data))
def process(self, model): """ Process the path model of a job and return each command within the job. Parameters ---------- model: QPainterPath The path to process Returns ------- generator: A list or generator object that yields each command to invoke on the device and the distance moved. In the format (distance, cmd, args, kwargs) """ config = self.config #: Previous point _p = QtCore.QPointF(self.origin[0], self.origin[1]) #: Do a final translation since Qt's y axis is reversed t = QtGui.QTransform.fromScale(1, -1) model = model*t #: Determine if interpolation should be used skip_interpolation = (self.connection.always_spools or config.spooled or not config.interpolate) # speed = distance/seconds # So distance/speed = seconds to wait step_size = config.step_size if not skip_interpolation and step_size <= 0: raise ValueError("Cannot have a step size <= 0!") try: # Apply device filters for f in self.filters: log.debug(" filter | Running {} on model".format(f)) model = f.apply_to_model(model) # Some versions of Qt seem to require a value in toSubpathPolygons m = QtGui.QTransform.fromScale(1, 1) polypath = model.toSubpathPolygons(m) # Apply device filters to polypath for f in self.filters: log.debug(" filter | Running {} on polypath".format(f)) polypath = f.apply_to_polypath(polypath) for path in polypath: #: And then each point within the path #: this is a polygon for i, p in enumerate(path): #: Head state # 0 move, 1 cut z = 0 if i == 0 else 1 #: Make a subpath subpath = QtGui.QPainterPath() subpath.moveTo(_p) subpath.lineTo(p) #: Update the last point _p = p #: Total length l = subpath.length() #: If the device does not support streaming #: the path interpolation is skipped entirely if skip_interpolation: x, y = p.x(), p.y() yield (l, self.move, ([x, y, z],), {}) continue #: Where we are within the subpath d = 0 #: Interpolate path in steps of dl and ensure we get #: _p and p (t=0 and t=1) #: This allows us to cancel mid point while d <= l: #: Now set d to the next point by step_size #: if the end of the path is less than the step size #: use the minimum of the two dl = min(l-d, step_size) #: Now find the point at the given step size #: the first point d=0 so t=0, the last point d=l so t=1 t = subpath.percentAtLength(d) sp = subpath.pointAtPercent(t) #if d == l: # break #: Um don't we want to send the last point?? #: -y because Qt's axis is from top to bottom not bottom #: to top x, y = sp.x(), sp.y() yield (dl, self.move, ([x, y, z],), {}) #: When we reached the end but instead of breaking above #: with a d < l we do it here to ensure we get the last #: point if d == l: #: We reached the end break #: Add step size d += dl #: Make sure we get the endpoint ep = model.currentPosition() yield (0, self.move, ([ep.x(), ep.y(), 0],), {}) except Exception as e: log.error("device | processing error: {}".format( traceback.format_exc())) raise e
def connection_lost(self): log.debug("protocol.connection_lost()")
def submit(self, job, test=False): """ Submit the job to the device. If the device is currently running a job it will be queued and run when this is finished. This handles iteration over the path model defined by the job and sending commands to the actual device using roughly the procedure is as follows: device.connect() model = device.init(job) for cmd in device.process(model): device.handle(cmd) device.finish() device.disconnect() Subclasses provided by your own DeviceDriver may reimplement this to handle path interpolation however needed. The return value is ignored. The live plot view will update whenever the device.position object is updated. On devices with lower cpu/gpu capabilities this should be updated sparingly (ie the raspberry pi). Parameters ----------- job: Instance of `inkcut.job.models.Job` The job to execute on the device test: bool Do a test run. This specifies whether the commands should be sent to the actual device or not. If True, the connection will be replaced with a virtual connection that captures all the command output. """ log.debug("device | submit {}".format(job)) try: #: Only allow one job at a time if self.busy: queue = self.queue[:] queue.append(job) self.queue = queue #: Copy and reassign so the UI updates log.info("Job {} put in device queue".format(job)) return with self.device_busy(): #: Set the current the job self.job = job self.status = "Initializing job" #: Get the time to sleep based for each unit of movement config = self.config #: Rate px/ms if config.custom_rate >= 0: rate = config.custom_rate elif self.connection.always_spools or config.spooled: rate = 0 elif config.interpolate: if config.step_time > 0: rate = config.step_size/float(config.step_time) else: rate = 0 # Undefined else: rate = from_unit( config.speed, # in/s or cm/s config.speed_units.split("/")[0])/1000.0 # Device model is updated in real time model = yield defer.maybeDeferred(self.init, job) #: Local references are faster info = job.info #: Determine the length for tracking progress whole_path = QtGui.QPainterPath() #: Some versions of Qt seem to require a value in #: toSubpathPolygons m = QtGui.QTransform.fromScale(1, 1) for path in model.toSubpathPolygons(m): for i, p in enumerate(path): whole_path.lineTo(p) total_length = whole_path.length() total_moved = 0 log.debug("device | Path length: {}".format(total_length)) #: So a estimate of the duration can be determined info.length = total_length info.speed = rate*1000 #: Convert to px/s #: Waiting for approval info.status = 'waiting' #: If marked for auto approve start now if info.auto_approve: info.status = 'approved' else: #: Check for approval before starting yield defer.maybeDeferred(info.request_approval) if info.status != 'approved': self.status = "Job cancelled" return #: Update stats info.status = 'running' info.started = datetime.now() self.status = "Connecting to device" with self.device_connection( test or config.test_mode) as connection: self.status = "Processing job" try: yield defer.maybeDeferred(self.connect) #: Write startup command if config.commands_before: yield defer.maybeDeferred(connection.write, config.commands_before) self.status = "Working..." #: For point in the path for (d, cmd, args, kwargs) in self.process(model): #: Check if we paused if info.paused: self.status = "Job paused" #: Sleep until resumed, cancelled, or the #: connection drops while (info.paused and not info.cancelled and connection.connected): yield async_sleep(300) # ms #: Check for cancel for non interpolated jobs if info.cancelled: self.status = "Job cancelled" info.status = 'cancelled' break elif not connection.connected: self.status = "connection error" info.status = 'error' break #: Invoke the command #: If you want to let the device handle more complex #: commands such as curves do it in process and handle yield defer.maybeDeferred(cmd, *args, **kwargs) total_moved += d #: d should be the device must move in px #: so wait a proportional amount of time for the device #: to catch up. This avoids buffer errors from dumping #: everything at once. #: Since sending is way faster than cutting #: we must delay (without blocking the UI) before #: sending the next command or the device's buffer #: quickly gets filled and crappy china piece cutters #: get all jacked up. If the transport sends to a spooled #: output (such as a printer) this can be set to 0 if rate > 0: # log.debug("d={}, delay={} t={}".format( # d, delay, d/delay # )) yield async_sleep(d/rate) #: TODO: Check if we need to update the ui #: Set the job progress based on how far we've gone if total_length > 0: info.progress = int(max(0, min(100, 100*total_moved/total_length))) if info.status != 'error': #: We're done, send any finalization commands yield defer.maybeDeferred(self.finish) #: Write finalize command if config.commands_after: yield defer.maybeDeferred(connection.write, config.commands_after) #: Update stats info.ended = datetime.now() #: If not cancelled or errored if info.status == 'running': info.done = True info.status = 'complete' except Exception as e: log.error(traceback.format_exc()) raise finally: if connection.connected: yield defer.maybeDeferred(self.disconnect) #: Set the origin if job.feed_to_end and job.info.status == 'complete': self.origin = self.position #: If the user didn't cancel, set the origin and #: Process any jobs that entered the queue while this was running if self.queue and not job.info.cancelled: queue = self.queue[:] job = queue.pop(0) #: Pull the first job off the queue log.info("Rescheduling {} from queue".format(job)) self.queue = queue #: Copy and reassign so the UI updates #: Call a minute later timed_call(60000, self.submit, job) except Exception as e: log.error(' device | Execution error {}'.format( traceback.format_exc())) raise