Skip to content

Commit

Permalink
Exclude Objects
Browse files Browse the repository at this point in the history
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
3 people committed Nov 21, 2021
1 parent 4eeb462 commit 89acce7
Show file tree
Hide file tree
Showing 6 changed files with 374 additions and 1 deletion.
8 changes: 8 additions & 0 deletions docs/Config_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1420,6 +1420,14 @@ Enable the "M118" and "RESPOND" extended
# override the "default_type".
```

### [exclude_object]
Enables support to exclude or cancel individual objects during the printing
process.

See the [exclude objects guide](Exclude_Object.md) and
[command reference](G-Codes.md#exclude-object)
for additional information.

## Resonance compensation

### [input_shaper]
Expand Down
106 changes: 106 additions & 0 deletions docs/Exclude_Object.md
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.
28 changes: 28 additions & 0 deletions docs/G-Codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,34 @@ is enabled (also see the [skew correction guide](skew_correction.md)):
SAVE_CONFIG gcode must be run to make the changes to peristent
memory permanent.


### Exclude Object
The following commands are available when an
[exclude_object config section](Config_Reference.md#exclude_object) is
enabled (also see the [exclude object guide](Exclude_Object.md)):

- `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.

- `DEFINE_OBJECT`: Provides a summary of an object in the file. It takes the following parameters:

- `NAME`: This parameter is required. It is the identifier used by other commands in this module.
- `CENTER`: An X,Y coordinate for the object.
- `POLYGON`: An array of X,Y coordinates that provide an outline for the object.

- `EXCLUDE_OBJECT`: This command takes a `NAME` parameter and instructs Klipper to ignore
gcode for that object.

- `LIST_OBJECTS`: Lists the objects known to Klipper. Without parameters, it will return a list of object
names. If the `VERBOSE` parameter is given (value doesn't matter), it will return object details.

- `LIST_EXCLUDED_OBJECTS`: Lists the excluded objects.

- `EXCLUDE_OBJECT_RESET`: Clears the current list objects and excluded objects.

### Delayed GCode

The following command is enabled if a
Expand Down
33 changes: 33 additions & 0 deletions docs/Status_Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,39 @@ The following information is available in the
forward direction minus the total number of steps taken in the
reverse direction since the micro-controller was last restarted.

## exclude_object

The following inforation is avaialbe in the [exclude_object](Exclude_Object.md) object:
- `objects`: An array of the known objects as provided by the `DEFINE_OBJECT` command. This is the same information
provided by the `LIST_OBJECTS` command in verbose mode. The `center` and `polygon` fields will not be present if
they weren't provided. Here is a JSON sample:
```
[
{
"polygon": [
[ 156.25, 146.2511675 ],
[ 156.25, 153.7488325 ],
[ 163.75, 153.7488325 ],
[ 163.75, 146.2511675 ]
],
"name": "CYLINDER_2_STL_ID_2_COPY_0",
"center": [ 160, 150 ]
},
{
"polygon": [
[ 146.25, 146.2511675 ],
[ 146.25, 153.7488325 ],
[ 153.75, 153.7488325 ],
[ 153.75, 146.2511675 ]
],
"name": "CYLINDER_2_STL_ID_1_COPY_0",
"center": [ 150, 150 ]
}
]
```
- `excluded_objects`: An array of strings listing the names of excluded objects.
- `current_object`: The name of the object currently being printed.

## fan

The following information is available in
Expand Down
197 changes: 197 additions & 0 deletions klippy/extras/exclude_object.py
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)
Loading

0 comments on commit 89acce7

Please sign in to comment.