-
-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
module: Exclude Objects Adding Klipper functionality to support cancelling objects while printing. This module keeps track of motion in and out of objects and adjusts movements as needed. It also tracks object status and provides that to clients. The Klipper module is relatively simple, and only provides one piece of the workflow. Support from Moonraker is underway to pre-process gcode files from various slicers with the extended gcode commands supplied by this module. UI's such as Fluidd and Mainsail will provide the user facing controls. There has been a small group sharing code. In addition to the Moonraker work, I have Fluidd code that will be submitted shortly. Mainsail support is also underway. Signed-off-by: Troy Jacobson <troy.d.jacobson@gmail.com> Co-authored-by: Franklyn <voron@tackitt.net> Co-authored-by: Eric Callahan <arksine.code@gmail.com>
- Loading branch information
1 parent
4eeb462
commit 89acce7
Showing
6 changed files
with
374 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
# Exclude Obects | ||
|
||
The `[exclude_object]` module allows Klipper to exclude objects while a print is in progress. | ||
To enable this feature include an [exclude_object config section](Config_Reference.md#exclude_object) | ||
(also see the [command reference](G-Codes.md#exclude-object).) | ||
|
||
Unlike other 3D printer firmware options, a printer running Klipper is utilizing a suite of | ||
components and users have many options to choose from. Therefore, in order to provide a | ||
a consistent user experience, the `[exclude_object]` moudle will establish a contract or API | ||
of sorts. The contract covers the contents of the gcode file, how the internal state of the | ||
module is controlled, and how that state is provided to clients. | ||
|
||
## Workflow Overview | ||
A typical worfklow for printing a file might look like this: | ||
1. Slicing is completed and the file is uploaded for printing. During the upload, the file | ||
is processed and `[exclude_object]` markup is added to the file. | ||
1. When printing starts, Klipper will reset the `[exclude_object]` status. | ||
1. When Klipper processes the `DEFINE_OBJECT` block, it will update the status with the known | ||
objects and pass it on to clients. | ||
1. The client may use that information to present a UI to the user so that progress can be | ||
tracked. Klipper will update the status to include the currently printing object which | ||
the client can use for display purposes. | ||
1. If the user requests that an object be cancelled, the client will issue an `EXCLUDE_OBJECT` | ||
command to Klipper. | ||
1. When Klipper process the command, it will add the object to the list of excluded objects | ||
and update the status for the client. | ||
1. The client will receive the updated status from Klipper and can use that information to | ||
reflect the object's status in the UI. | ||
1. When printing finishes, the `[exclude_object]` status will continue to be available until | ||
another action resets it. | ||
|
||
## The GCode File | ||
The specialized GCode processing needed to support exlucing objects does not fit into Klipper's | ||
core design goals. Therefore, this module requires that the file is processed before being sent | ||
to Klipper for printing. Using a post-process script in the slicer or having middleware process | ||
the file on upload are two possibilities for preparing the file for Klipper. | ||
|
||
### GCode File Command Reference | ||
`DEFINE_OBJECT`: Provides a summary of an object in the file. Objects don't need to be defined | ||
in order to be referenced by other commands. The primary purpose of this command is to provide | ||
information to the UI without needing to parse the entire gcode file. | ||
|
||
It takes the following parameters: | ||
|
||
- `NAME`: This parameter is required. It is the identifier used by other commands in this module. | ||
The name must be unique among all objects in the file being printed, and must be consistent across all layers. | ||
- `CENTER`: An X,Y coordinate for the object. Typically it will be in the center of the object, but | ||
that is not a requirement. While this parameter is technically optional, not including it will | ||
likely limit the functionality of other components. Example: `CENTER=150.07362,138.27616`. | ||
- `POLYGON`: An array of X,Y coordinates specifying vertices that define a polygon outline for the object. | ||
The polygon information is primarly for the use of graphical interfces. This parameter is optional, but | ||
like `CENTER`, the functionality of other components may be reduced if it is not given. It is left to the | ||
software processing the gcode file to determine the complexity of the polygon being provided. At a | ||
minimum, it is recommended that this be a bounding box. | ||
Example: `POLYGON=[[142.7,130.95],[142.7,145.75],[157.5,145.75],[157.5,130.95]]` | ||
|
||
`START_CURRENT_OBJECT`: This command takes a `NAME` parameter and denotes the start of | ||
the gcode for an object on the current layer. | ||
|
||
`END_CURRENT_OBJECT`: Denotes the end of the object's gcode for the layer. It is paired with | ||
`START_CURRENT_OBJECT`. A `NAME` parameter is optional. A warning will be given if | ||
an `END_CURRENT_OBJECT` command is encountered when it wasn't expected or of the given | ||
name does not match the current object. | ||
|
||
## Managing Excluded Objects | ||
The `EXCLUDE_OBJECT` command is used to request that Klipper stops printing the specified object. | ||
The command may be executed at any time and Klipper will track the object name until the status is | ||
reset. This command may be executed manually, but will often be part of the exclude object implementation | ||
of a client. | ||
|
||
`LIST_OBJECTS`, `LIST_EXCLUDED_OBJECTS`, and `EXCLUDE_OBJECT_RESET` commands are also available | ||
|
||
### Command Reference | ||
`EXCLUDE_OBJECT`: This command takes a `NAME` parameter and instructs Klipper to ignore | ||
gcode that is marked by `START_CURRENT_OBJECT` and `END_CURRENT_OBJECT` for the named | ||
object. The command can be issued for the currently printing object. In that case, Klipper will | ||
immediately move on to the next object. An object can be marked for exclusion before Klipper | ||
encounters it in the file. | ||
|
||
`LIST_OBJECTS`: Lists the objects known to Klipper. | ||
|
||
`LIST_EXCLUDED_OBJECTS`: Lists the excluded objects. | ||
|
||
`EXCLUDE_OBJECT_RESET`: Resets the state of the `[exclude_object]` module. This clears the lists | ||
containing known objects, cancelled objects, and the name of the current object. | ||
|
||
## Status Infomation | ||
The state of this module is provided to clients by the [exclude_object status](Status_Reference.md#exclude_object). | ||
|
||
The status is reset when: | ||
- The Klipper firmware is restarted. | ||
- There is a reset of the `[virtual_sdcard]`. Notable, this is reset by Klipper at the start of a print. | ||
- When an `EXCLUDE_OBJECT_RESET` command is issued. | ||
|
||
The list of defined objects is represented in the `exclude_object.objects` status field. In a well defined | ||
gcode file, this will be done with `DEFINE_OBJECT` commands at the beginning of the file. This will provide | ||
clients with object names and coordinates so the UI can provide a graphical representation of the objects if | ||
desired. | ||
|
||
As the print progresses, the `exclude_object.current_object` status field will be updated as Klipper processes | ||
`START_CURRENT_OBJECT` and `END_CURRENT_OBJECT` commands. The `current_object` field will be set even if the | ||
object has been excluded. | ||
|
||
As `EXCLUDE_OBJECT` commands are issued, the list of excluded objects is provided in the `exclude_object.excluded_objects` | ||
array. Since Klipper looks ahead to process upcoming gcode, there may be a delay betwen when the command is | ||
issued and when the status is updated. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
# Exclude moves toward and inside objects | ||
# | ||
# Copyright (C) 2019 Eric Callahan <arksine.code@gmail.com> | ||
# Copyright (C) 2021 Troy Jacobson <troy.d.jacobson@gmail.com> | ||
# | ||
# This file may be distributed under the terms of the GNU GPLv3 license. | ||
|
||
import logging | ||
import json | ||
from datetime import datetime | ||
|
||
class ExcludeObject: | ||
def __init__(self, config): | ||
self.printer = config.get_printer() | ||
self.gcode = self.printer.lookup_object('gcode') | ||
self.gcode_move = self.printer.load_object(config, 'gcode_move') | ||
self.printer.register_event_handler("sdcard:reset_file", | ||
self._reset_file) | ||
self.next_transform = None | ||
self.objects = {} | ||
self.excluded_objects = [] | ||
self.current_object = None | ||
self.in_excluded_region = False | ||
self.last_position = [0., 0., 0., 0.] | ||
self.last_position_extruded = [0., 0., 0., 0.] | ||
self.last_position_excluded = [0., 0., 0., 0.] | ||
self.gcode.register_command( | ||
'START_CURRENT_OBJECT', self.cmd_START_CURRENT_OBJECT, | ||
desc=self.cmd_START_CURRENT_OBJECT_help) | ||
self.gcode.register_command( | ||
'END_CURRENT_OBJECT', self.cmd_END_CURRENT_OBJECT, | ||
desc=self.cmd_END_CURRENT_OBJECT_help) | ||
self.gcode.register_command( | ||
'EXCLUDE_OBJECT', self.cmd_EXCLUDE_OBJECT, | ||
desc=self.cmd_EXCLUDE_OBJECT_help) | ||
self.gcode.register_command( | ||
'EXCLUDE_OBJECT_RESET', self.cmd_EXCLUDE_OBJECT_RESET, | ||
desc=self.cmd_EXCLUDE_OBJECT_RESET_help) | ||
self.gcode.register_command( | ||
'DEFINE_OBJECT', self.cmd_DEFINE_OBJECT, | ||
desc=self.cmd_DEFINE_OBJECT_help) | ||
self.gcode.register_command( | ||
'LIST_OBJECTS', self.cmd_LIST_OBJECTS, | ||
desc=self.cmd_LIST_OBJECTS_help) | ||
self.gcode.register_command( | ||
'LIST_EXCLUDED_OBJECTS', self.cmd_LIST_EXCLUDED_OBJECTS, | ||
desc=self.cmd_LIST_EXCLUDED_OBJECTS_help) | ||
def _setup_transform(self): | ||
if not self.next_transform: | ||
logging.debug('Enabling ExcludeObject as a move transform') | ||
self.next_transform = self.gcode_move.set_move_transform(self, | ||
force=True) | ||
def _reset_file(self): | ||
self.objects = {} | ||
self.excluded_objects = [] | ||
self.current_object = None | ||
if self.next_transform: | ||
logging.debug('Disabling ExcludeObject as a move transform') | ||
self.gcode_move.set_move_transform(self.next_transform, force=True) | ||
self.next_transform = None | ||
|
||
def get_position(self): | ||
self.last_position[:] = self.next_transform.get_position() | ||
self.last_delta = [0., 0., 0., 0.] | ||
return list(self.last_position) | ||
|
||
def _normal_move(self, newpos, speed): | ||
self.last_position_extruded[:] = newpos | ||
self.last_position[:] = newpos | ||
self.next_transform.move(newpos, speed) | ||
|
||
def _ignore_move(self, newpos, speed): | ||
self.last_position_excluded[:] = newpos | ||
self.last_position[:] = newpos | ||
return | ||
|
||
def _move_into_excluded_region(self, newpos, speed): | ||
logging.debug("Moving to excluded object: %s", | ||
(self.current_object or "---")) | ||
self.in_excluded_region = True | ||
self.last_position_excluded[:] = newpos | ||
self.last_position[:] = newpos | ||
|
||
def _move_from_excluded_region(self, newpos, speed): | ||
logging.debug("Moving to included object: %s", | ||
(self.current_object or "---")) | ||
logging.debug("last position: %s", | ||
" ".join(str(x) for x in self.last_position)) | ||
logging.debug("last extruded position: %s", | ||
" ".join(str(x) for x in self.last_position_extruded)) | ||
logging.debug("last excluded position: %s", | ||
" ".join(str(x) for x in self.last_position_excluded)) | ||
logging.debug("New position: %s", " ".join(str(x) for x in newpos)) | ||
if self.last_position[0] == newpos[0] and \ | ||
self.last_position[1] == newpos[1]: | ||
# If the X,Y position didn't change for this transitional move, | ||
# assume that this move should happen at the last extruded location | ||
newpos[0] = self.last_position_extruded[0] | ||
newpos[1] = self.last_position_extruded[1] | ||
newpos[3] = newpos[3] - self.last_position_excluded[3] + \ | ||
self.last_position_extruded[3] | ||
logging.debug("Modified position: " + " ".join(str(x) for x in newpos)) | ||
self.last_position[:] = newpos | ||
self.last_position_extruded[:] = newpos | ||
self.next_transform.move(newpos, speed) | ||
self.in_excluded_region = False | ||
|
||
def _test_in_excluded_region(self): | ||
# Inside cancelled object | ||
if self.current_object in self.excluded_objects: | ||
return True | ||
|
||
def get_status(self, eventtime=None): | ||
status = { | ||
"objects": list(self.objects.values()), | ||
"excluded_objects": list(self.excluded_objects), | ||
"current_object": self.current_object | ||
} | ||
return status | ||
|
||
def move(self, newpos, speed): | ||
move_in_excluded_region = self._test_in_excluded_region() | ||
|
||
if move_in_excluded_region: | ||
if self.in_excluded_region: | ||
self._ignore_move(newpos, speed) | ||
else: | ||
self._move_into_excluded_region(newpos, speed) | ||
else: | ||
if self.in_excluded_region: | ||
self._move_from_excluded_region(newpos, speed) | ||
else: | ||
self._normal_move(newpos, speed) | ||
|
||
cmd_START_CURRENT_OBJECT_help = "Marks the beginning the current object" \ | ||
" as labeled" | ||
def cmd_START_CURRENT_OBJECT(self, params): | ||
name = params.get('NAME').upper() | ||
self.current_object = name | ||
cmd_END_CURRENT_OBJECT_help = "Markes the end the current object" | ||
def cmd_END_CURRENT_OBJECT(self, gcmd): | ||
if self.current_object == None: | ||
gcmd.respond_info("END_CURRENT_OBJECT called, but no object is" | ||
" currently active") | ||
return | ||
name = gcmd.get('NAME', default=None) | ||
if name != None and name.upper() != self.current_object: | ||
gcmd.respond_info("END_CURRENT_OBJECT NAME=%s does not match the" | ||
" current object NAME=%s" % | ||
(name.upper(), self.current_object)) | ||
self.current_object = None | ||
cmd_EXCLUDE_OBJECT_help = "Cancel moves inside a specified objects" | ||
def cmd_EXCLUDE_OBJECT(self, params): | ||
name = params.get('NAME').upper() | ||
if name not in self.excluded_objects: | ||
self.excluded_objects.append(name) | ||
cmd_EXCLUDE_OBJECT_RESET_help = "Resets the exclude_object state by" \ | ||
" clearing the list of object definitions" \ | ||
" and removed objects" | ||
def cmd_EXCLUDE_OBJECT_RESET(self, params): | ||
self._reset_file() | ||
cmd_LIST_OBJECTS_help = "Lists the known objects" | ||
def cmd_LIST_OBJECTS(self, gcmd): | ||
if gcmd.get('VERBOSE', None) is not None: | ||
object_list = " ".join (str(x) for x in self.objects.values()) | ||
else: | ||
object_list = " ".join(self.objects.keys()) | ||
gcmd.respond_info(object_list) | ||
cmd_LIST_EXCLUDED_OBJECTS_help = "Lists the excluded objects" | ||
def cmd_LIST_EXCLUDED_OBJECTS(self, gcmd): | ||
object_list = " ".join (str(x) for x in self.excluded_objects) | ||
gcmd.respond_info(object_list) | ||
cmd_DEFINE_OBJECT_help = "Provides a summary of an object" | ||
def cmd_DEFINE_OBJECT(self, params): | ||
self._setup_transform() | ||
|
||
name = params.get('NAME').upper() | ||
center = params.get('CENTER', default=None) | ||
polygon = params.get('POLYGON', default=None) | ||
|
||
obj = { | ||
"name": name, | ||
} | ||
|
||
if center != None: | ||
c = [float(coord) for coord in center.split(',')] | ||
obj['center'] = c | ||
|
||
if polygon != None: | ||
obj['polygon'] = json.loads(polygon) | ||
|
||
logging.debug('Object %s defined %r', name, obj) | ||
self.objects[name] = obj | ||
|
||
|
||
def load_config(config): | ||
return ExcludeObject(config) |
Oops, something went wrong.