printrun-src/printrun/laser.py

Sat, 04 Jun 2016 09:22:51 +0200

author
mbayer
date
Sat, 04 Jun 2016 09:22:51 +0200
changeset 20
03b34402d405
parent 19
234037fbca4b
child 21
8551b89bd05e
permissions
-rw-r--r--

Code cleanup

"""
Lasercutter library
2015/2016 by NeoSoft, Malte Bayer
Intended to use standalone or implemented in Pronterface/Printrun
"""

"""
LASERCUT SETTINGS
TODO: move to printrun settings
"""
ENGRAVE_SPEED = 10 * 60 # mm/min
# 30mm/min works for wood (regulate the output power to something between 10-30%)
# 30mm/min for black anodized aluminum to get a light engraving @ 100% power
# 10mm/min for black anodized aluminum to get more "silver" @ 100% power

TRAVEL_SPEED = 120 * 60
E_FACTOR = 0.5

# BITMAP:
DPI = 300
GREY_THRESHOLD = 0
CHANGE_DIRECTION = True
INVERT_CUT = True

"""
STATIC DEFINITIONS
DO NOT CHANGE WORLD's RULES!
"""
INCH = 25.4 # mm
MM_PIXEL = round(INCH / DPI, 4)
STEPS_PIXEL = MM_PIXEL * 80 # mine is 80 steps/mm on XY

# FOR HPGL:
SCALE_FACTOR = 1.0 / 40.0 # 40 plotter units

# GENERAL HEADER AND FOOTER GCODE
GCODE_HEAD = """
; GCode generated by laser.py pronterface library (marlin code flavour)
; 2015/2016 by NeoSoft - Malte Bayer

G21 ; Metric
; We assume Z is in focus height and laser head is focus at bottom left of image!
G92 X0 Y0 E0; set zero position - new origin
G90 ; absolute positioning
M82 ; Set extruder (laser) to absolute positioning
M201 X1000 Y1000 E1000 ; Set acceleration
M203 X1000 Y1000 Z4 E1000 ; Set max feedrate
M209 S0 ; disable firmware retraction, we dont want to burn holes...
M302 ; Allow cold extrudes - doesnt matter because we hack the extruder physically off with the M571 E mod
M571 S1 E1 ; Activate Laser output on extrusion, but block real motor movement!
G0 X0 Y0 F%d ; Set moving speed TRAVEL_SPEED
G1 X0 Y0 F%d ; Set linear engraving speed ENGRAVE_SPEED

""" % (TRAVEL_SPEED, ENGRAVE_SPEED)

GCODE_FOOT = """M400 ; Wait for all moves to finish
M571 S0 E0
M42 P28 S0 ; Force laser off!
M501 ; undo all settings made
"""

from PIL import Image
import sys

# Imports for SVG
import xml.etree.ElementTree as ET
import math
from svg2gcode import shapes as shapes_pkg
from svg2gcode.shapes import point_generator


class Lasercutter:
    """
    Lasercutter methods
    parameters: log = logger function (fuction has to accept a string)
    """
    def __init__(self, pronterwindow = None):
        if pronterwindow:
            self.pronterwindow = pronterwindow
            self.log = pronterwindow.log
            self.pronterwindow.clear_log(None)
        else:
            self.pronterwindow = None
            self.log = lambda : None
        self.log("Lasercutter library initialized\n%d DPI (%f mm/pixel)" % (DPI, MM_PIXEL))
        if STEPS_PIXEL <= 5:
            self.log("WARNING: STEPS PER PIXEL NEEDS TO BE > 5 (otherwise marlin joins lines): %f" % STEPS_PIXEL)
        self.log("Travel/Engrave speed: %d mm/sec, %d mm/sec" % (
            TRAVEL_SPEED / 60, ENGRAVE_SPEED / 60) )
        self.log("")
        
    
    def pixel2bit(self, pixel, threshold=128):
        """Convert the pixel value to a bit."""
        # some really weird stuff here ;-P

        # RGB to greyscale
        #print pixel
        #print type(pixel)
        if isinstance(pixel, tuple):
            #rgb
            pixel = pixel[0]*0.2989 + pixel[1]*0.5870 + pixel[2]*0.1140
            threshold = 128
            if pixel > threshold:
                return 1
            else:
                return 0

        # color palette
        if pixel <= threshold:
            return 1
        else:
            return 0

    def image2gcode(self, filename):
        """
        Open a image file and get the basic information about it.
        Then convert it to gcode (replacing the existing gcode buffer contents)
        """
        try:
            im = Image.open(filename)
        except:
            self.log("Unable to open %s" % filename)
            return False

        self.log("Converting Image for lasercut:")
        self.log("File: %s" % filename)
        self.log("format: %s, mode: %s" % (im.format, im.mode))
        width,height = im.size
        self.log("size: %d x %d pixels" % im.size)

        pix = im.load()

        fo = open(filename + ".g", "w")
        fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD))

        fo.write(";Start engraving the raster image: %dx%d points @ %d DPI = %.0fx%.0f mm\n\n" % (
            im.size[0], im.size[1], DPI, im.size[0]*MM_PIXEL, im.size[1]*MM_PIXEL) )

        INVERT_Y = MM_PIXEL * (im.size[1] -1) * (-1)
        DIR = 1
        for X in range(im.size[0]):
            fo.write("; X=%d printing row: direction %i\n" % (X, DIR))
            fo.write("G92 E0\n")
            E = 0
            last_bit = 1 # we engrave on black pixel = 0
            START_Y = 0
            if DIR > 0:
                range_start = 0
                range_stop = im.size[1]
            else:
                range_start = im.size[1] -1
                range_stop = -1

            for Y in range(range_start, range_stop, DIR):
                YMM = abs((Y * MM_PIXEL) + INVERT_Y)
                XMM = X * MM_PIXEL
                #print "X %d Y %d" % (X, Y)
                bit = self.pixel2bit(pix[X, Y], GREY_THRESHOLD)
                if INVERT_CUT:
                    if bit == 0:
                        bit = 1
                    else:
                        bit = 0
                if last_bit == bit:
                    if bit == 1:
                        # nothing to do,
                        continue
                    else:
                        # are we at the end of Y range?
                        #print Y
                        if (Y == (im.size[1] - 1)) or (Y == 0):
                            # draw line
                            if DIR > 0:
                                E = E + MM_PIXEL * (Y - START_Y)
                            else:
                                E = E + MM_PIXEL * (START_Y - Y)
                            fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR))
                else:
                    # bit value has changed!
                    if bit == 0:
                        # jump to start of line to write
                        START_Y = Y
                        fo.write("G0 X%.4f Y%.4f\n" % (XMM, YMM))
                    else:
                        # end of line to write
                        if DIR > 0:
                            E = E + (MM_PIXEL * (Y - START_Y))
                        else:
                            E = E + (MM_PIXEL * (START_Y - Y))
                        fo.write("G1 X%.4f Y%.4f E%.4f\n" % (XMM, YMM, E * E_FACTOR))
                last_bit = bit
            if CHANGE_DIRECTION:
                DIR = DIR * (-1) # change y direction on every X

        fo.write(GCODE_FOOT)
        fo.close()

        if self.pronterwindow:
            self.log("")        
            self.pronterwindow.load_gcode_async(filename + '.g')

    def hpgl2gcode(self, filename):
        OFFSET_X = 0.0
        OFFSET_Y = 0.0

        self.log("Converting HPGL plot for lasercut:")
        self.log("File: %s" % filename)
    
        fi = open(filename, "r")
        fo = open(filename + ".g", "w")
        fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD))

        G = "0"
        LASER_STATE = 0
        last_coord = [0.0,0.0] 
        last_cmd = ""

        for line in fi.readlines():
            for action in line.split(";"):
                action = action.strip()
                if action != "":
                    cmd = action[:2]
                    if cmd == "PD":
                        LASER_STATE = 1
                    elif cmd == "PU":
                        LASER_STATE = 0
                        if last_cmd == "PD":
                            OFFSET_X = coord[0] * -1
                            OFFSET_Y = coord[1] * -1
                            fo.write("; PD PU detected, set coord offset %.4f x %.4f mm\n" % (OFFSET_X, OFFSET_Y))
                    elif cmd == "PA" or cmd == "PR":
                        # TODO: convert relative coordinates to absolute here!
                        coord = action[2:].split(",")
                        coord[0] = (float(coord[0]) + OFFSET_X) * SCALE_FACTOR
                        coord[1] = (float(coord[1]) + OFFSET_Y) * SCALE_FACTOR
                        if LASER_STATE:
                            EN = " E%.4f F%.4f" % (
                              E_FACTOR *  math.hypot(coord[0] - last_coord[0], coord[1] - last_coord[1]),
                              ENGRAVE_SPEED * 0.5 ) # 1/2 engraving speed
                        else:
                            EN = " F%.4f" % TRAVEL_SPEED
                            
                        fo.write("G%d X%.4f Y%.4f%s\n" % (
                            LASER_STATE, coord[0], coord[1], EN) )
                        last_coord = coord
                    elif cmd == "IN":
                        pass
                    elif cmd == "PT":
                        print "Ignoring pen thickness"                
                    else:
                        print "UNKNOWN: %s" % action
                    last_cmd = cmd            
    
        fo.write(GCODE_FOOT)
        fi.close()
        fo.close()    

        if self.pronterwindow:
            self.log("")        
            self.pronterwindow.load_gcode_async(filename + '.g')


    def svg2gcode(self, filename, bed_max_x = 50, bed_max_y = 50, smoothness = 0.2):
        self.log("Generating paths from SVG...")        

        shape_preamble = "G92 E0\n"
        shape_postamble = ""

        """ 
        Used to control the smoothness/sharpness of the curves.
        Smaller the value greater the sharpness. Make sure the
        value is greater than 0.1
        """
        if smoothness < 0.1: smoothness = 0.1
        
        svg_shapes = set(['rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path'])
    
        tree = ET.parse(filename)
        root = tree.getroot()
    
        width = root.get('width')
        height = root.get('height')
        if width == None or height == None:
            viewbox = root.get('viewBox')
            if viewbox:
                _, _, width, height = viewbox.split()                

        if width == None or height == None:
            self.log("Unable to get width and height for the svg!")
            return False

        width = float(width.replace("px", ""))
        height = float(height.replace("px", ""))

        scale_x = bed_max_x / max(width, height)
        scale_y = bed_max_y / max(width, height)
        
        self.log("Scaling factor: %.2f, %.2f" % (scale_x,scale_y))

        fo = open(filename + ".g", "w")
        fo.write("; Filename: %s\n%s" % (filename, GCODE_HEAD))

        for elem in root.iter():
            try:
                _, tag_suffix = elem.tag.split('}')
            except ValueError:
                continue

            if tag_suffix in svg_shapes:
                shape_class = getattr(shapes_pkg, tag_suffix)
                shape_obj = shape_class(elem)
                d = shape_obj.d_path()
                m = shape_obj.transformation_matrix()

                if d:
                    fo.write("M400 ; wait for moves finish, then printing shape: %s\n" % (tag_suffix))
                    E = 0
                    xo = 0
                    yo = 0
                    fo.write(shape_preamble) 
                    p = point_generator(d, m, smoothness)
                    start = True
                    for x,y,pen in p:
                        y = height - y
                        xs = scale_x * x
                        ys = scale_y * y
                        if xo == xs and yo == ys: continue

                        if not pen: start = True 
                        if xs >= 0 and xs <= bed_max_x and ys >= 0 and ys <= bed_max_y:
                            if start:
                                fo.write("G0 X%0.2f Y%0.2f F%.4f ; Move to start of shape\n" % (xs, ys, TRAVEL_SPEED))
                                start = False
                                xo = xs
                                yo = ys                                
                            else:  
                                e_distance = math.hypot(xs - xo, ys - yo)
                                xo = xs
                                yo = ys                                
                                E = E + (e_distance)
                                fo.write("G1 X%0.2f Y%0.2f E%.4f F%.4f\n" % (xs, ys, E * E_FACTOR, ENGRAVE_SPEED))
                        else:
                            self.log("Position outside print dimension: %d, %d" % (xs, ys)) 
                    fo.write(shape_postamble)

        fo.write(GCODE_FOOT)
        fo.close()
                     
        if self.pronterwindow:
            self.log("")        
            self.pronterwindow.load_gcode_async(filename + '.g')
                        

mercurial