14.2.13

Quick Rebuttal

Michal Zalewski makes a number of good points in 3D Printing Revolution: the Complex Reality, but glosses over:
  • additive manufacturing can make shapes that CNC milling and other subtractive processes cannot, especially given the disparity in forces needed between working metal and plastic - anyone who mounts a cutting tool on their 3d printer knows what I mean
  • skilled operators and CAD software are sufficient to create designs, but CAD combined with simulation software and less skilled labor are probably also sufficient - especially when the simulation includes modelling the fabrication process and compute power will only get cheaper
  • long tail applications may drive more demand than the industrial designs he thinks are necessary - no one wants to make nail clippers, they want to make custom iPhone cases
  • the current open loop control systems will inevitably be replaced by feedback control systems as more low cost sensors become available - why hasn't anyone integrated cheap Chinese scale DROs into a printer yet?
  • getting people to think about manufacturing their own stuff and avoiding the mass produced supply chain is a political struggle and far more important than the initial success or failure of particular technologies - he's coming from a big business "Maker" mentality, when the real action is with "Crafter" folk
Just my 2¢.

3.2.13

Math

How many steps does the printer take to get up to speed? For example, if we have 78.74 steps/mm in X and Y (0.012mm resolution), and the maximum velocity is 120 mm/sec as set in software (Cura), and the maximum acceleration is 9000 mm/sec2 in the Marlin firmware, how many steps does it take to ramp up to the maximum velocity, and what is the corresponding distance?

We assume a trapezoidal velocity profile, i.e. constant acceleration until the maximum velocity is reached, starting from zero velocity and finishing at 120mm/sec. So with an acceleration of 9000 mm/sec2 the time taken to reach maximum velocity is 120/9000 is 0.0133 seconds (13.3 milliseconds). At the start the stepper is not stepping (0 steps per second) and at the end is stepping at 120×78.74 = 9449 steps per second or one step every 0.106 milliseconds.

From basic physics (anyone remember that?) the total distance is found by integrating under the steps per second velocity function. The number of steps to get up to speed can be found by just integrating over the time taken to reach the maximum velocity. That's the pretty blue area in the diagram. Because it is a linear function, the integration is just calculating the area of the triangle (half the base times the height) under the curve, or 0.5×0.0133×9449 = 63 steps, which is 63/78.74 = 0.8 mm.

The takeaway here is that with a 0.4 mm nozzle diameter, it's up to speed within two diameters.

So how does the F variable, the feedrate, in the GCode file correspond to the velocity. Here’s an example:

G1 X91.427 Y90.0 F3600.0 E3.2357

The 3600 is 30 times the Cura specified 120 mm/sec. Why? The answer is that the feed rate is in mm/minute, so this is 60 mm/sec. Why is it off by a factor of two then? Because I grabbed an example from the first few lines of the file (the bottom layer) which has a different maximum velocity (set to 60 mm/sec). If I look further down in the GCode file for example it is 120×60 = 7200 as expected:

G1 X93.376 Y97.826 F7200.0 E2.1045

Some notes:

The theoretical machine maximum velocity is 250.0 mm/sec in the Marlin firmware, but I don’t know anybody who prints that fast.

Some firmware will change from the previous command’s feed rate to the one specified in the current command as it does the move, but the Marlin firmware just sets the feed rate as a constant for the entire move. This means the top line in the trapezoidal profile is always horizontal for Marlin, but other firmware may have a sloped line.

The acceleration value is clipped based on the maximum acceleration of each axis – including Z and E – so the actual acceleration might be less than the example value used above.

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()

20.1.13

Thing Tracker

The Thing Tracker Network got me thinking about exactly what would need to be shared. This immediately brought to mind the classic science fiction work "The Big Front Yard" by Clifford Simak, describing how interstellar trade must be in ideas and information and not material objects. The protagonist trades the "idea of paint" for the "idea of anti-gravity saddles".

What ideas do we have to trade?

I fall back to my design roots, and this forces me to ask what are the use-cases we wish to address? Let me start out with a few off the top of my head.

  • Replacement Part

    Aside from trivial handles and feet, printing replacement parts will probably need the participation of manufacturers. Some, like Nokia, have started this process already. But if you've ever been associated with product lifecycle management, you'll know there is a lot more to it than a one-time publication. Specifically, different product models, sizes, component part differences, configurations, geographies (line voltages, certification, and so on), versions over time, etc. make the correct part difficult to determine even for the manufacturers building it. All of this information would need to be included with the tracked object.

  • Custom Object

    Although there are ways to tailor objects after construction, there will be a use-case for things that are specifically built to fit people, pets, rooms, vehicles, etc. The big problem here is measuring and applying the measurements correctly to the objects. For this purpose, the instructions and parameters would need to be included with the tracked object so it's useful to the receiver.

  • Toys, Jewelry, Art

    By and large, this is the current set of objects, that requires only geometry, material, and some images and assembly instructions.

The current state of affairs is pretty crude. The existing sites like Thingiverse, Physible Exchange, The Pirate Bay, Archive of Our Own, etc. share only .stl files, or whatever else the author chooses to share. These are doomed eventually, because they

  1. don't have full parametric model files
  2. have insufficient metadata
  3. aren't indexed, other than by full text search or user specified ad-hoc tags
  4. aren't versioned
  5. are centralized to one site

The Thing Tracker may answer some of these. For example, it may be decentralized ... as long as the implementation uses something like torrent magnet files. I think the content and use-cases need to be determined and then the implementation will derive naturally from those. Are there any use-cases I omitted?

Carbon Offsetting

I don't trust CO2 carbon offset schemes. Just like every other charity, there is an efficiency rating for the process, and in my opinion the amount of good being done is minimal, if not negative.

Rather than try to ameliorate the situation with a band-aid solution, just use less fossil fuels in the first place. If you can't avoid flying, take it easy on the environment by bringing less with you.

For those who have to sit on their suitcases to close them, there is no salvation. But for normal people consider that it takes 2.5 liters of jet fuel to ship an extra kilogram across the Atlantic. A CO2 conversion chart says 2.52kg of CO2 is produced for every liter of jet fuel. This means you can do 6.3kg of CO2 offsetting by just not carrying an extra kilo.

To get that fuel consumption number took a little digging, and I present it here so others can ratify it if they want.

From Figure 4.8 Fuel Efficiency, the industry achieved 1630 Revenue Ton-Miles / 1000 liters in 2006. Revenue Ton-Miles is a measure of cargo amount. It is calculated by multiplying the weight in tons of the shipment being transported by the number of miles that it is transported. This metric includes only paid tonnage. In other words, only materials being transported for and being paid for by customers. Using 1.60934 kilometers/mile and 907.185 kg/ton, this works out to 4.2e-4 liters / kilogram-kilometer. For 6000 km trans Atlantic flight, you get about 2.5 liters / kilogram.

To put this in perspective, one penny is 2.35g, which needs 0.006 liters to cross the Atlantic. Using the current IATA Jet Fuel price of $3.08/(US)gallon (€0.61/liter), it costs nearly half a cent (0.48¢) to ship a penny. The airlines would be more environmentally friendly by converting your pocket change into paper bills just before you step on the aircraft. They could do this at a very customer advantageous exchange rate and still make money.

The airlines are on the right track in reducing luggage allowance and charging €75 for the extra 23kg piece of luggage ($100/50lbs). The next step is to charge per kilo of person. Fortunately for me, they don't do that yet.

The plane manufacturers can obviously do a lot more too, since 50-80% of the takeoff weight of an aircraft is just the plane.

13.1.13

Friction Welding

Following on post and a YouTube video by Fran, I tried my hand at friction welding. It works, but you need to practice.

Chuck up a straightened piece of ABS filament about five centimeter long in a Dremel tool such that about three or four centimeters are visible. Turn on the dremel tool at medium speed and drag the spinning plastic backwards along the joint to be welded making little circular motions.

The plastic will liquefy, there's a bit of a burnt plastic smell and then the parts are joined. The laminations of the printed part may come apart if you're too rough with it, so practice on some scraps first.

Follow welding best practices where possible, i.e. it's best in a corner between two pieces to be joined, and avoid butt welding like that shown below.

Backlash

Yesterday I discovered a significant amount of backlash in my Ultimaker's axes. This is how I fixed it.

First I isolated where the backlash was coming from. I used a bit of blue tape and stuck it to shafts and belts in the mechanical linkage while driving the axis back and forth with the smallest step in PrintRun. The reason for the blue tape is to amplify the small angular or linear motions enough to see them.

Starting at the motor shaft, observe the motion of the blue tape while clicking the small step left and right (X) or up and down (Y) in PrintRun. If you observe motion in both directions move to the next part in the linkage (pulley, belt, shaft). At some point in the process, the blue tape doesn't move unless you click the step button several times. The joint between the current part and the previous part is the problem.

For me, this was right at the beginning, either where the pulleys are set-screwed onto the motor shafts or the small pulley tension is set by the four motor mounting screws. So I took off the motor, removed and reattached the pulley on the motor shaft (to let the set screw get a new grip on the shaft), and reassembled it with a lot more tension in the small belts. I was able to remove most of the backlash just from that.

This also explains the flat spots I was getting when using G2 and G3 arc codes.