Python

From Cricut Hacking Wiki
Jump to: navigation, search

Python Cutter class

The following class should get any Python programmer started communicating with the Cricut. This code requires at least Python 2.6 and may require some minor changes to function in Python 3.x. Windows users will need pyserial. Users with First Generation Hardware who want built-in support for Command 0x40 will need pyXXTEA, but this is an optional dependency; if you don't have pyXXTEA, Cutter.cmd40 will simply throw NotImplementedError.

import random, math, serial
try:
    import pyxxtea
except:
    pyxxtea = None

class Cutter(serial.Serial):
    # Constants for cmd40
    CutLine = 0
    CutCurve = 1
    MoveTo = 2
    LowerAndCutLine = 3
    CutLineAndRaise = 4
    LowerAndCutCurve = 5
    CutCurveAndRaise = 6
    DecryptKey = 7

    # XXTEA encryption keys
    KEY0 = [ 0x27, 0x2D, 0x6C, 0x37, 0x34, 0x2A, 0x61, 0x73, 0x36, 0x63, 0x25, 0x5B, 0x2B, 0x26, 0x5A, 0x4D ]
    KEY1 = [ 0x7D, 0x31, 0x6E, 0x22, 0x4A, 0x4A, 0x71, 0x33, 0x5A, 0x3C, 0x5C, 0x5F, 0x78, 0x61, 0x3A, 0x61 ]
    KEY2 = [ 0x47, 0x30, 0x2A, 0x23, 0x5D, 0x31, 0x48, 0x2F, 0x3B, 0x25, 0x7A, 0x61, 0x36, 0x71, 0x38, 0x2F ]
    KEY3 = [ 0x30, 0x3F, 0x68, 0x63, 0x71, 0x64, 0x6D, 0x30, 0x47, 0x69, 0x45, 0x7B, 0x6D, 0x34, 0x25, 0x69 ]
    KEY4 = [ 0x45, 0x35, 0x66, 0x50, 0x3A, 0x38, 0x6D, 0x69, 0x57, 0x5A, 0x70, 0x37, 0x33, 0x5F, 0x35, 0x7D ]
    KEY5 = [ 0x34, 0x3A, 0x21, 0x48, 0x61, 0x4F, 0x39, 0x25, 0x75, 0x3F, 0x69, 0x53, 0x47, 0x46, 0x36, 0x26 ]
    KEY6 = [ 0x3F, 0x62, 0x62, 0x6D, 0x7E, 0x55, 0x5F, 0x44, 0x7E, 0x29, 0x42, 0x5A, 0x52, 0x24, 0x62, 0x68 ]
    KEY7 = [ 0x47, 0x30, 0x2A, 0x23, 0x34, 0x2A, 0x61, 0x73, 0x47, 0x69, 0x45, 0x7B, 0x33, 0x5F, 0x35, 0x7D ]
    keys = (KEY0,KEY1,KEY2,KEY3,KEY4,KEY5,KEY6,KEY7)

    # Internal storage
    _model = None
    _verMaj = None
    _verMin = None

    def mkstr(*l):
        if type(l[0]) == list or type(l[0]) == tuple:
            l = l[0]
        rv = bytes()
        for i in l: rv = rv + chr(i)
        return rv

    def cmd(self, *args):
        """Writes a command to the device. The length will be calculated from the input, so do not include a length byte."""
        self.write(Cutter.mkstr(len(args), *args))
        rlen = ord(self.read(1))
        rv = self.read(rlen)
        return rv
    def cmd40(self, c, x, y):
        """Writes an 0x40 command to the device. The first parameter is one of Cutter.CutLine, Cutter.CutCurve, Cutter.MoveTo,
           Cutter.LowerAndCutLine, Cutter.CutLineAndRaise, Cutter.LowerAndCutCurve, Cutter.CutCurveAndRaise, or Cutter.DecryptKey.
           The second and third parameters are the X and Y coordinates of the command."""
        if pyxxtea is None:
            raise NotImplementedError()
        r = random.randint(10000,32767)
        plaintext = Cutter.mkstr(0, 0, r/256, r%256, 0, 0, x/256, x%256, 0, 0, y/256, y%256)
        cipher = pyxxtea.XXTEA(Cutter.mkstr(self.keys[c])).encrypt(plaintext)
        b = [ord(ch) for ch in cipher]
        self.write(Cutter.mkstr(13, 0x40, *b))
        rlen = self.read(1)
        return self.read(ord(rlen))
    def bounds(self):
        """Returns the mat bounds as (xmin, ymin, xmax, ymax)."""
        b = [ord(ch) for ch in self.cmd(0x11,0,0,0)]
        return b[0]*256+b[1], b[2]*256+b[3], b[4]*256+b[5], b[6]*256+b[7]
    def hasMat(self):
        """Returns True if a mat is loaded, False otherwise."""
        b = self.cmd(0x14,0,0,0,0)
        return ord(b[3]) & 0x01 and True or False
    def _cacheModel(self):
        b = [ord(ch) for ch in self.cmd(0x12,0,0,0)]
        self._model = b[0]*256+b[1]
        self._verMaj = b[2]*256+b[3]
        self._verMin = b[4]*256+b[5]
    def model(self):
        """Returns the model ID of the device."""
        if self._model is None:
            self._cacheModel();
        return self._model
    def version(self):
        """Returns the firmware version of the device."""
        if self._model is None:
            self._cacheModel();
        return self._verMaj + self._verMin * math.pow(10, -math.ceil(math.log(self._verMin,10)))

Python sample code

These code examples have only been tested with Second Generation Hardware but the appropriate protocols for First Generation Hardware are implemented and should work.

Windows

import cutter
# The first parameter is the port number, minus one. This example opens COM4.
s = cutter.Cutter(3, timeout=1, baudrate=198347)

Linux

For First Generation Hardware you must have the FTDI serial port driver installed. For Second Generation Hardware you must have the USB ACM driver installed, although many mainstream Linux distributions have this provided by default.

import cutter
# First generation hardware appears as a USB serial port
s = cutter.Cutter('/dev/ttyUSB0', timeout=1, baudrate=198347)
# Second generation hardware appears as an ACM device
s = cutter.Cutter('/dev/ttyACM0', timeout=1)

Mac

For First Generation Hardware you must install the FTDI VCP driver. OSX has the necessary ACM drivers preinstalled. import cutter

# Second generation hardware appears as a modem.
# You will find tty.usbmodem* and cu.usbmodem* nodes in /dev/. Update the filename with the correct number.
# It is currently unclear whether the "tty" or "cu" version works better, or if there's even a difference. The official CraftRoom software uses "cu".
# For first generation hardware, add baudrate=198347 to the parameter list.
s = cutter.Cutter('/dev/cu.usbmodem411', timeout=1)

Example usage

print "The attached Cricut reports model 0x%x and firmware version %s." % (s.model(), s.version())
status = s.cmd(0x14, 0x00, 0x00, 0x00, 0x00) # or use s.hasMat() for access to the one documented status bit