27.1.13

Hand Crafted G Code

The hair-brained idea I had to make a heat exchanger has yielded first artifacts.
The idea is to use two multi-section 3D printed half pipes for counter-flow air handling. The two half pipes would share a common bulkhead down the middle which would be perforated by diamond shaped holes. The holes would line up between the two half pipes, and be separated from each other by bits of tin foil glued to the edges to keep the air streams from mixing and provide good heat transfer. The two half pipes glued like this into one round pipe would be joined with others like it into a meter long (or more) exchanger, wrapped with insulation (fiberglass pink maybe) and the whole thing stuffed in a PVC water pipe for structural integrity.
In the original sketch, shown at the right, I was more worried about the method used for interlocking the sections and making sure there were no bumps along the bulkhead. Small sections are needed to accommodate the limited build area of the 3D printer. As it turns out the sections hold together well enough with just the texture of the plastic layers rubbing together, so the little raised bump on the lip and matching interior depression aren't really needed.
The big problem was, I discovered I couldn't use any of the existing slicing programs. There were two problems to overcome. One was getting the walls thin enough, and the other was keeping the G-code file size and generation time manageable. Even though plastic is a good insulator, you want thin walls so the heat isn't transmitted down the pipe and also so that it takes the least amount of plastic to create the pipes as possible. Some preliminary work I did when using the usual workflow of creating a 3D model, exporting it as an STL file, and then slicing the STL file to G-code was giving complete garbage multi-megabyte files and taking on the order of hours for simple structures.
So I decided to write my own program. For the wall thickness problem, you can craft it so that the wall thickness is exactly two extrudate traces wide. For the file size problem, using the G2 and G3 arc codes supported by the Marlin firmware makes for a very compact - and exact - representation of the geometry. Slicing programs are not able to use G2 and G3 because they start from an already tessellated STL file and hence would have to "recreate" the arc information from adjacent planar faces. What crap.
So I wrote the program in Python, to kill two birds with one stone - learn a little Python, and generate the g-code files for the pipes.
This is the first Python program I've written, so please forgive any idiotic constructs you see in the code below. It's not generic in any way. My excuse is that it's just enough to get my job done. The results are shown at left, with a couple of sections 50mm long and 40mm in diameter. The intent is to eventually make them four times bigger, otherwise the pressure losses from the air turbulence would be too great.
But the first problem is to figure out why one side of the hole is malformed as the printer climbs the 45° angle...


# coding=utf-8
'''
Pipe generator.
Created on January 27, 2013
@author: Derrick Oswald
'''
import math
import sys
import textwrap

class Machine:
    """A class representing parameters of a additive manufacturing 3D printer."""
    # g code preamble
    preamble = textwrap.dedent("""\
        M92 E865.888000    ; set axis_steps_per_unit to calibrated value
        M109 S210.000000   ; set extruder temperature and wait
        G21                ; metric values
        G90                ; absolute positioning
        M107               ; start with the fan off
        G28 X0 Y0          ; move X/Y to min endstops
        G28 Z0             ; move Z to min endstops
        G92 X0 Y0 Z0 E0    ; reset software position to front/left/z=0.0
        G1 Z15.0 F420
        G92 E0             ; zero the extruded length
        G1 F200 E3         ; extrude 3mm
        G92 E0             ; zero the extruded length again
        ; start""")
    # g code postamble
    postamble = textwrap.dedent("""\
        ; end
        M104 S0            ; extruder heater off
        M140 S0            ; heated bed heater off (if you have it)
        G91                ; relative positioning
        G1 Z2.5 E-5 F9000  ; heads up retracting
        G28 X0 Y0          ; move X/Y to min endstops, so the head is out of the way
        M84                ; steppers off
        G90                ; absolute positioning""")
    nozzle = 0.4 # nozzle diameter (mm)
    filament = 2.89 # filament diameter (mm)
    width = 0.5 # trace width or more likely the minimum wall thickness (mm)
    xorigin = 100.0 # X origin location (mm)
    yorigin = 100.0 # Y origin location (mm)
    zorigin = 0.0 # Z origin location (mm)
    eorigin = 0.0 # E origin location (mm)
    fast = 9000.0 # fast (slew) speed (mm/sec)
    slow = 3600.0 # slow (print) speed (mm/sec)
    def computeExtrudateCrossSecionalArea (self, layer = 0.1):
        # compute the area of the extrudate as a squashed circle cross section
        # with a thickness of layer and two rounded (bulgy) ends for a total of width
        return (layer * (self.width - layer) + (math.pi * (layer / 2.0) * (layer / 2.0)))

class Pipe:
    """A class representing the pipe to be created."""
    radius = 20.0 # inner radius of pipe (mm)
    wall = 2 # number of traces in the wall thickness
    length = 8.0 # pipe length (mm)
    overlap = 3.0 # pipe overlap joint size (mm)
    def computeR (self, machine, z = 0.0):
        thickness = self.wall * machine.width
        if z < self.overlap:
            r = self.radius + thickness
        elif z < self.overlap + thickness:
            r = self.radius + thickness - (z - self.overlap) # linear 45° interpolation
        else:
            r = self.radius
        return(r);
    def prepare (self, machine, job):
        area = machine.computeExtrudateCrossSecionalArea (job.layer)
        filament = machine.filament / 2.0 # radius
        x = machine.xorigin
        y = machine.yorigin
        z = machine.zorigin;
        e = machine.eorigin;
        print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' F' + str(machine.fast) + ' ; goto center stage'
        # draw a box around the working area by clearance amount
        halfedge = self.radius + job.clearance
        edge = halfedge * 2.0
        x += halfedge
        z += job.layer
        vol = (area * edge) / (math.pi * filament * filament) # E movement required to extrude a full edge
        print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' F' + str(machine.fast) + ' ; draw a box'
        y -= halfedge
        e += vol / 2.0
        print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
        x -= edge
        e += vol
        print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
        y += edge
        e += vol
        print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
        x += edge
        e += vol
        print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
        y -= halfedge
        e += vol / 2.0
        print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
        return (e)
    def generate(self, machine, job):
        if '' != machine.preamble:
            print >> job.output, machine.preamble
        e = self.prepare (machine, job)
        filament = machine.filament / 2.0 # radius
        area = machine.computeExtrudateCrossSecionalArea (job.layer)
        x = machine.xorigin
        y = machine.yorigin
        z = machine.zorigin;
        while z < self.length:
            z += job.layer
            radius = self.computeR(machine, z)
            for i in range (self.wall):
                r = radius + machine.width * i + machine.width / 2.0
                x = machine.xorigin + r
                y = machine.yorigin + 0.0
                print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' F' + str(machine.fast)
                trace = math.pi * r
                vol = (area * trace) / (math.pi * filament * filament) # E movement required to extrude a half circle
                e += vol
                x -= 2.0 * r
                print >> job.output, 'G3 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' I' + str(-r) + ' J' + str(0.0) + ' F' + str(machine.slow)
                e += vol
                x += 2.0 * r
                print >> job.output, 'G3 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' I' + str(+r) + ' J' + str(0.0) + ' F' + str(machine.slow)
        if '' != machine.postamble:
            print >> job.output, machine.postamble

class HalfPipe (Pipe):
    """A class representing the half pipe to be created."""
    gapedge = 2.0 # gap limiting edge width (mm)
    def computeD(self, machine, z, i):
        thickness = self.wall * machine.width
        d = thickness - (machine.width / 2.0) - (i * machine.width)
        startz = self.length - (self.overlap + thickness)
        if z > startz:
            delta = z - startz # linear 45° interpolation
            if (delta > thickness):
                delta = thickness
            d += delta
        return(d)
    def computeGap(self, machine, z): # actually the half-gap
        thickness = self.wall * machine.width
        startz = self.overlap + thickness
        endz = self.length - (self.overlap + thickness)
        maximum = self.computeR(machine, z) + machine.width / 2.0 - self.gapedge
        if ((z > startz) and (z < endz)):
            glo = z - startz
            ghi = endz - z
            if (ghi < glo):
                g = ghi
            else:
                g = glo
            if (g > maximum):
                g = maximum
        else:
            g = 0.0
        return (g)
    def generate(self, machine, job):
        if '' != machine.preamble:
            print >> job.output, machine.preamble
        e = self.prepare (machine, job)
        filament = machine.filament / 2.0 # radius
        area = machine.computeExtrudateCrossSecionalArea (job.layer)
        x = machine.xorigin
        y = machine.yorigin
        z = machine.zorigin;
        while z < self.length:
            z += job.layer
            radius = self.computeR(machine, z)
            gap = self.computeGap(machine, z)
            if ((0 != gap) and (2 == self.wall)):
                r0 = radius +                 machine.width / 2.0
                r1 = radius + machine.width + machine.width / 2.0
                d0 = self.computeD(machine, z, 0)
                d1 = self.computeD(machine, z, 1)
                dx0 = math.sqrt(r0 * r0 - d0 * d0)
                dx1 = math.sqrt(r1 * r1 - d1 * d1)
                x = machine.xorigin + dx0
                y = machine.yorigin + d0
                vol0 = (area * math.pi * (r0 - 2 * d0)) / (math.pi * filament * filament) # E movement required to extrude an inner half circle, approximately
                vol1 = (area * math.pi * (r1 - 2 * d1)) / (math.pi * filament * filament) # E movement required to extrude an outer half circle, approximately
                vol2 = (area * 2 * dx0) / (math.pi * filament * filament) # E movement required to extrude an inner diameter
                vol3 = (area * 2 * dx1) / (math.pi * filament * filament) # E movement required to extrude an outer diameter
                vol4 = (area * machine.width) / (math.pi * filament * filament) # E movement required to extrude across from inner to outer wall
                print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' F' + str(machine.fast)
                e += vol0
                x -= 2 * dx0
                print >> job.output, 'G3 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' I' + str(-dx0) + ' J' + str(-d0) + ' F' + str(machine.slow)
                e += vol2 * (dx0 - gap) / dx0
                x += dx0 - gap
                print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
                e += vol4
                y -= machine.width
                print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
                e += vol3 * (dx1 - gap) / dx1
                x -= dx1 - gap
                print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
                e += vol1
                x += 2 * dx1
                print >> job.output, 'G2 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' I' + str(dx1) + ' J' + str(-d1) + ' F' + str(machine.slow)
                e += vol3 * (dx1 - gap) / dx1
                x -= dx1 - gap
                print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
                e += vol4
                y += machine.width
                print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
                e += vol2 * (dx0 - gap) / dx0
                x += dx0 - gap
                print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
            else:
                for i in range (self.wall):
                    r = radius + machine.width * i + machine.width / 2.0
                    d = self.computeD(machine, z, i)
                    dx = math.sqrt(r * r - d * d)
                    x = machine.xorigin + dx
                    y = machine.yorigin + d
                    vol = (area * math.pi * (r - 2 * d)) / (math.pi * filament * filament) # E movement required to extrude a half circle, approximately
                    vol2 = (area * 2 * dx) / (math.pi * filament * filament) # E movement required to extrude a diameter
                    print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' F' + str(machine.fast)
                    e += vol
                    x -= 2 * dx
                    print >> job.output, 'G3 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' I' + str(-dx) + ' J' + str(-d) + ' F' + str(machine.slow)
                    e += vol2
                    x += 2.0 * dx
                    print >> job.output, 'G1 X' + str(x) + ' Y' + str(y) + ' Z' + str(z) + ' E' + str(e) + ' F' + str(machine.slow)
        if '' != machine.postamble:
            print >> job.output, machine.postamble
    
class Job:
    """Parameters related to a specific print job."""
    output = sys.stdout # target for print
    layer = 0.1 # layer height (mm)
    clearance = 5.0 # offset outside of object for box (mm)

if __name__ == '__main__':
    machine = Machine ()
    pipe = HalfPipe ()
    pipe.radius = 20.0
    pipe.length = 50.0
    pipe.wall = 2
    pipe.overlap = 4.0
    job = Job()
    job.output = file ('output.gc', 'w')
    job.layer = 0.1
    job.clearance = 5.0
    pipe.generate (machine, job)
    job.output.flush()
    job.output.close()

No comments:

Post a Comment