py_silhouette: Control Silhouette plotters/cutters from Python

py_silhouette is a Python library for controlling the Silhouette series of desktop plotters/cutters.

This library is intended to serve two purposes:

  • It is intended to form the basis of both general and special purpose plotting software.

  • To document the outcome of a reverse engineering effort for the protocol used to control Silhouette plotters.

This library is not:

  • A complete plotting tool – it is just a library.

  • A general purpose plotter control library – it only controls the Silhouette series of desktop plotters.

  • A library of generic utilities for plotting – it only contains low-level device control functionality.

  • A complete reverse engineering of every hardware command – it contains enough to implement all advertised device functionality though some software emulation of certain functions may be required (e.g. for manual head movement control).

Quick-and-dirty example

The following complete example illustrates how this library could be used to draw a simple 20mm x 10mm rectangle:

from py_silhouette import SilhouetteDevice

# Connect to first available device
d = SilhouetteDevice()

# Move to (10, 10) mm as a starting point without drawing
d.move_to(10, 10, False)

# Draw the rectangle
d.move_to(30, 10, True)
d.move_to(30, 20, True)
d.move_to(10, 20, True)
d.move_to(10, 10, True)

# Finish plotting and return to the home position (don't forget this!)
d.move_home()

# Flush the command buffer and wait for all commands to be acknowledged
# (nothing will happen until this is called)
d.flush()

A better example

The minimal example above will work but has numerous shortcomings. Specifically:

  • If multiple devices are connected, one will be chosen arbitrarily.

  • Printed registration marks will be ignored.

  • The speed and force applied by the device is undefined.

  • If using a cutting tool, corners will not be cut correctly.

To improve this example we should first present the user with a choice of devices by using enumerate_devices() to discover what is available:

from py_silhouette import SilhouetteDevice, enumerate_devices

devices = list(enumerate_devices())
for num, (usb_device, device_params) in enumerate(devices):
    print("{}: {}".format(num, device_params.name))

num = int(input("Choose a device number to use: "))
usb_device, device_params = devices[num]

d = SilhouetteDevice(usb_device, device_params)

Next we can use SilhouetteDevice.zero_on_registration_mark() to zero the device’s coordinate system on a set of printed registration marks (which we’ll assume in this example mark a 200x100mm area):

from py_silhouette import RegistrationMarkNotFoundError

try:
    d.zero_on_registration_mark(200, 100)
except RegistrationMarkNotFoundError:
    print("Registration marks not found! Continuing without...")

Next, we can inform the device of the tool’s diameter which will ensure corners will be cut out correctly using SilhouetteDevice.set_tool_diameter() and tool information in SilhouetteDevice.params.tool_diameters:

d.set_tool_diameter(d.params.tool_diameters["Knife"])

Next we’ll choose what speed and force we wish to use, again choosing parameters based on the DeviceParameters object in DeviceParameters.params:

d.set_speed(d.params.tool_speed_min)
d.set_force(d.params.tool_force_max)

Now we’re ready to cut out the rectangle:

d.move_to(10, 10, False)

d.move_to(30, 10, True)
d.move_to(30, 20, True)
d.move_to(10, 20, True)
d.move_to(10, 10, True)

d.move_home()

d.flush()

The SilhouetteDevice.flush() method will block untli all commands have been received into the plotters buffer and so will probably return before plotting has actually completed. We can use SilhouetteDevice.get_state() to wait until the plotter has actually finished plotting (i.e. is nolonger in the DeviceState.moving state):

import time
from py_silhouette import DeviceState

while d.get_state() == DeviceState.moving:
    time.sleep(0.5)

print("Cutting complete!")

API

The complete API is contained within the py_silhouette module. The principal class, SilhouetteDevice, represents a connection to a plotter connected via USB and provides a number of low-level methods for controling the device (e.g. SilhouetteDevice.move_to()). A number of supporting functions and data structures are defined which descover or describe available devices and may be used to construct or configure a SilhouetteDevice (e.g. enumerate_devices()).

Device Discovery & Connection

To construct a SilhouetteDevice we must first discover a connected device for it to control using enumerate_devices():

py_silhouette.enumerate_devices(supported_device_parameters=SUPPORTED_DEVICE_PARAMETERS)

Generator which produces a series of (device, device_params) pairs for all currently connected devices.

Parameters
supported_device_parameters[DeviceParameters, …]

An optional list of DeviceParameters objects for the types of devices to include in the enumeration. By default this is all of the devices enumerated in SUPPORTED_DEVICE_PARAMETERS.

At this point you may wish to present your users with a prompt to select a plotter (perhaps using DeviceParameters.name as a hint) or select one automatically according to your own logic (perhaps using DeviceParameters.area_width_min and friends to make an informed choice).

Once a specific device has been chosen, pass the USB device and DeviceParameters object to the SilhouetteDevice constructor:

class py_silhouette.SilhouetteDevice(device=None, device_params=None)

Connect to and control the specified plotter/cutter.

See enumerate_devices() for discovering connected devices and their parameters.

As a convenience, if no arguments are provided, this class will attempt to connect to the first device found by enumerate_devices(), throwing a NoDeviceFoundError if no devices are found.

Parameters
devicepyusb.core.Device

The USB device object for the plotter to control.

device_paramsDeviceParameters

Definition of the device’s key parameters.

The DeviceParameters used to configure the device can be obtained from:

SilhouetteDevice.params = None

The DeviceParameters object passed during construction (or selected automatically). Contains useful information about the shape and size of media and tools supported by this device.

For diagnostic purposes you can request the device name and state:

SilhouetteDevice.get_name()

Return the human-readable name and version reported by the device (as a string).

SilhouetteDevice.get_state()

Get the current state of the device returning a DeviceState.

class py_silhouette.DeviceState(value)

What is the device currently doing?

ready = b'0'

The device is ready to begin plotting.

moving = b'1'

The plotter is busy plotting or moving.

unloaded = b'2'

Idle with no media loaded.

paused = b'3'

The pause button has been pressed.

unknown = None

Unrecognised device state; probably an error.

Setting the plotter origin

Before beginning a plot it is important to decide how the coordinate axis is zeroed. This library presents you with two options:

SilhouetteDevice.zero_on_registration_mark(width, height, box_size=5.0, line_thickness=0.5, line_length=20.0, search=True)

Zero coordinate system and compensate for small page misalignments using registration marks printed on the page.

If the registration marks are not found, RegistrationMarkNotFoundError is raised.

This command will block until the registration marks have been found or not.

The registration settings will be retained until the current page is ejected from the machine.

Warning

As a side effect of calling this command, the tool speed will be set to its maximum.

Note

Registration marks should look as follows (without the red construction/dimension lines…):

The registration marks consist of a 5x5mm square at the top left of the page, a 20mm 'L' bracket at the bottom left and corresponding bracket at the top right.
  • The registration marks must be oriented as shown and white space must be left around all three marks to ensure they are found by the plotter.

  • The entire path to be plotted/cut must be within the bounds of the registration marks.

  • The top-left mark should be near the top-left of the page so that the plotter can find.

  • The width and height are measured from the outside of the corner bracket lines.

Most Silhouette plotters also support a second type of registration mark where the top-left square is replaced with another corner bracket. Use of this type of registration mark is not supported by this library.

Parameters
width, height: float

The size of the area the registration mark covers in mm.

Warning

Take care that the right-most registration mark is not too close to the right-hand extreme of the machine. The registration sensor is mounted at the very left side of the carriage so it will need to move further than it would when plotting on that corner of the page. The plotter does not have a ‘right’ end-stop and may hit the end of its axis.

box_sizefloat

The size of the black square in the top-left registration mark (mm). Currently must be set to 5mm (the default).

line_thicknessfloat

The thickness of the registration lines (mm). Default of 0.5 mm is known to work well.

line_lengthfloat

The length the registration lines (mm). Default of 20 mm is known to work well.

searchbool

If true, the device will start searching for the registration mark automatically, starting at the device home position. If False the device should first be positioned with the tool over the black square. In practice this is very difficult to achieve so most users will want to leave this setting in its default mode (True).

Setting tool parameters

Prior to plotting it is important to set the plotting speed, force and tool parameters according to the tool and material in use. There are no hard-and-fast rules for setting these parameters so experimentation is required.

SilhouetteDevice.set_speed(speed)

Set the movement speed of the device in mm/sec.

This parameter will be automatically clamped to the range specified in the SilhouetteDevice.params.tool_speed_min and SilhouetteDevice.params.tool_speed_max.

SilhouetteDevice.set_force(force)

Set the amount of force to be applied (in grams (yes.)) when the tool is used.

This parameter will be automatically clamped to the range specified in the SilhouetteDevice.params.tool_force_min and SilhouetteDevice.params.tool_force_max.

Depending on the type of tool used, the device will automatically tweak the toolpath supplied to compensate for the tool diameter. For this function to work correctly, the diameter of the tool must also be supplied.

SilhouetteDevice.set_tool_diameter(diameter)

Inform the plotter of the diameter of a swivelling tool’s working point to allow it to adjust tool paths accordingly.

Tool diameters for the standard tools supplied with the current device can be obtained from SilhouetteDevice.params.tool_diameters.

Note

Cutting blade cartridges contain a blade on a swivelling attachment, a little like the casters on an office chair.

A story-board showing a corner being cut in four steps. Step 1-2: The knife reaches the corner. Step 2-3: The plotter moves the knife such that the point stays stationary but is now pointing along the next edge. Step 3-4: The next edge is cut.

As such, the point of the blade’s position will lag behind the plotter’s position. Setting this parameter causes the device’s firmware to compensate for this automatically when turning corners by moving the plotter in an arc pattern towards the new line. During this move, the blade turns to face the new cut direction but does not actually cut.

When using a tool with a swivelling cutting implement (such as the included knife cartridge), setting this parameter correctly is strongly recommended for good results. If using a fixed implement (e.g. a pen), this setting should usually be set to 0.0 since the pen tip is fixed.

Parameters
diameterfloat

Tool swivel mounting diameter.

This parameter will be automatically clamped to the range specified in the SilhouetteDevice.params.tool_diameter_min and SilhouetteDevice.params.tool_diameter_max.

Finally, for devices with an auto blade, the following function may be used to automatically set the blade depth.

SilhouetteDevice.set_depth(depth)

Set the blade depth on devices supporting auto blade.

This parameter will be automatically clamped to the range specified in the SilhouetteDevice.params.tool_depth_min and SilhouetteDevice.params.tool_depth_max.

If the device does not have auto blade support (i.e. SilhouetteDevice.params.tool_depth_min is None), an AutoBladeNotSupportedError will be raised.

Plotting

Plotting is performed by making a series of SilhouetteDevice.move_to() calls followed by SilhouetteDevice.move_home() and SilhouetteDevice.flush().

SilhouetteDevice.move_to(x, y, use_tool)

Move the plotter, optionally with the tool engaged.

Facing the plotter, the X axis runs from left to right with strictly positive coordinates. The Y axis runs from top to bottom with strictly positive coordinates.

Call flush() to ensure this command has arrived at the device.

After completing a sequence of move_to commands, always use move_home() to return the plotter to the home position and notify the device that plotting has finished.

Parameters
x, y: float

Absolute page position in mm.

These values will be clamped between 0 and the maximum page width and height however this may not always be enough to prevent the machine hitting the end of the carrage (e.g. when zeroed on a registration mark). It is the caller’s responsibility to sensibly clip the input to prevent crashes.

If zero_on_registration_mark() has been used since the last paper load, coordinates will be relative to the top-left corner of the registration mark and should not go beyond the width and height of the registered area. If zero_on_registration_mark() has not been used, coordinates start from the device’s home position.

use_tool: bool

If True, the tool will be applied during the movement. If False, the tool will be lifted.

SilhouetteDevice.move_home()

Move the carriage to the home position (or to the top-left registration mark if zeroed) with the tool disengaged.

The plotter expects this to be the final command received at the end of a sequence of move_to() calls.

Note

If this command is not used at the end of a series of move_to() calls, the final command sent will be delayed for a short while since the device likes to always have a look-ahead of at least one command (probably to support tool diameter compensation – see set_tool_diameter()).

Call flush() to ensure this command has arrived at the device.

SilhouetteDevice.flush()

Ensure all outstanding commands have been sent. Blocks until complete.

Device Parameters and Tools

Device parameters for widely used Silhouette devices are included in:

py_silhouette.SUPPORTED_DEVICE_PARAMETERS

A list of DeviceParameters describing a particular supported device. At the time of writing, only support for the Silhouette Potrait v1 has been verified.

For each supported device type, information defining its USB interface identifiers, plotter dimensions and out-of-the-box tool support is included.

class py_silhouette.DeviceParameters(product_name, usb_vendor_id, usb_product_id, area_width_min, area_width_max, area_height_min, area_height_max, tool_diameters=_Nothing.NOTHING, tool_force_min=7.0, tool_force_max=231.0, tool_speed_min=100.0, tool_speed_max=1000.0, tool_diameter_min=0.0, tool_diameter_max=2.3, tool_depth_min=None, tool_depth_max=None)

Method generated by attrs for class DeviceParameters.

product_name

Human readable product name for the device supported by this class.

usb_vendor_id

The USB Vendor ID used by device.

usb_product_id

The USB Product ID used by device.

area_width_min

Minimum width for valid plot areas (mm)

area_width_max

Maximum width for valid plot areas (mm)

area_height_min

Minimum height for valid plot areas (mm)

area_height_max

Maximum height for valid plot areas (mm)

tool_diameters

A dictionary mapping from tool name to tool diameter (in mm) for all tools which ship with or are available for this device for use with SilhouetteDevice.set_tool_diameter(). Generally 'Pen' and 'Knife' tools will be defined.

tool_force_min

Lowest force which may be applied by the machine (in grams)

tool_force_max

Highest force which may be applied by the machine (in grams)

tool_speed_min

Lowest speed at which the machine can move (mm/sec)

tool_speed_max

Highest speed at which the machine can move (mm/sec)

tool_diameter_min

Lowest valid tool diameter (mm)

tool_diameter_max

Highest valid tool diameter (mm)

tool_depth_min

Shortest valid knife setting, or None if automatic depth setting is not possible for this device.

tool_depth_max

Longest valid knife setting, or None if automatic depth setting is not possible for this device.

Exceptions

The following exceptions may be thrown by this library.

exception py_silhouette.DeviceError

Baseclass for all py_silhouette hardware related errors.

exception py_silhouette.NoDeviceFoundError

No connected devices were found.

exception py_silhouette.RegistrationMarkNotFoundError

The registration mark could not be found.

exception py_silhouette.AutoBladeNotSupportedError

Thrown when SilhouetteDevice.set_depth() is called on a device without auto blade support.

Origins and Acknowledgements

This software is primarily based on my own reverse engineering efforts targeting the Silhouette Portrait USB protocol based on observing the behaviour of the Silhouette Studio software running under Windows back in 2013. This first reverse-engineering pass uncovered an easy to use and understand subset of the Silhouette control protocol allowing the device to be satisfactorily used under Linux and other platforms.

Later, I discovered others’ efforts to reverse engineer and drive the Silhouette series of plotters (in particular Robocut and later Inkscape-Silhouette). Based hints in these other codebases I managed to document the remaining ‘unknowns’ within my minimal subset of the USB protocol used by Silhouette Studio.

I’m also greatful to derwassi on GitHub for reverse engineering and assisting with testing of the auto-blade feature.