The Smell of Molten Projects in the Morning

Ed Nisley's Blog: Shop notes, electronics, firmware, machinery, 3D printing, laser cuttery, and curiosities. Contents: 100% human thinking, 0% AI slop.

Category: Software

General-purpose computers doing something specific

  • Thing-O-Matic: Skeinforge Reversal Failure

    As it turned out, though, that part wasn’t the first attempt.

    Caliper part - heavy blobbing
    Caliper part – heavy blobbing

    Even switching to red filament didn’t help:

    Extrusion blob - top view
    Extrusion blob – top view

    That, in fact, was when the light dawned: it always failed at exactly the same point for a given set of G-Code.

    Come to find out that, for some parts printed with certain options, the Skeinforge Reversal plugin dependably produces huge blobs of plastic after a move. The extruder reverses properly, the XY stages move, then the extruder starts running forward at the Reversal speed while the XY stages move at whatever rate they’re supposed to for the next thread, producing a prodigious blob.

    Extrusion blob - side view
    Extrusion blob – side view

    Most parts have much more interior than they do exterior and, with any luck, the blobs vanish inside. However, this little bitty thing has no room to hide a blob. Several parts went down the drain, but at least it had a countable number of layers!

    Here’s a sample of the failure:

    G1 X0.0 Y2.11 Z3.3 F708.318
    M108 R25.0       <--- Reversal speed
    M101             <--- Extruder on forward
    G04 P125.0       <--- Reversal pause
    M108 R0.3778     <--- Normal speed (continues forward)
    G1 X0.0 Y2.19 Z3.3 F354.159
    G1 X-0.66 Y2.11 Z3.3 F354.159
    G1 X-0.66 Y2.29 Z3.3 F354.159
    G1 X-1.32 Y2.29 Z3.3 F354.159
    G1 X-1.32 Y2.11 Z3.3 F354.159
    G1 X-1.98 Y2.29 Z3.3 F354.159
    G1 X-2.64 Y2.29 Z3.3 F354.159
    G1 X-2.64 Y-3.4 Z3.3 F354.159
    G1 X-1.98 Y-3.4 Z3.3 F354.159
    M108 R25.0       <--- Reversal speed
    M102             <--- Extruder on reverse
    G04 P125.0       <--- Reversal pause
    M103             <--- Extruder off
    G1 X-3.3 Y2.11 Z3.3 F708.318 <--- move to next thread
    M101             <--- Extruder on forward
    G1 X-3.3 Y2.29 Z3.3 F354.159 <--- BLOB FAIL
    G1 X-3.96 Y2.28 Z3.3 F354.159
    G1 X-3.96 Y2.11 Z3.3 F354.159
    M103             <--- Extruder off
    

    The pcregrep progam (do a sudo apt-get install pcregrep on Ubuntu) can find the blob-causing sequences after you generate the G-Code:

    pcregrep -n -M 'G04.*\nM103\nG1.*\nM101\nG1' Caliper.gcode
    

    You need that program, because ordinary grep only searches within a single line. In this case, the G-Code pattern extends over several lines. The pcre stands for Perl Compatible Regular Expressions and the -M turns on multi-line matching.

    The results look like this:

    905:G04 P125.0
    M103
    G1 X-3.3 Y2.11 Z3.3 F708.318
    M101
    G1 X-3.3 Y2.29 Z3.3 F354.159
    1101:G04 P125.0
    M103
    G1 X-3.3 Y2.13 Z3.96 F651.721
    M101
    G1 X-3.3 Y2.29 Z3.96 F325.861
    

    You can count the number of blobs with the -cl options.

    Having found the blobs, edit the file, jump to the indicated lines, copy the nearest preceding forward extruder move, including the speed setting, and paste it in front of the M101 that starts the extruder. If my sed-fu were stronger, I could automate that process.

    Unleashing pcregrep on my collection of G-Code files shows a bunch of ’em with blobs and a few without. Note that this has nothing to do with the firmware running on the printer, because the G-Code has the error.

    What happens, I think, is that Reversal emits a correct reverse at the end of a thread, does a fast move to the start of the next thread, notices that (at least) the first G1 of the new thread falls below the length threshold that would activate the un-reversal action, and incorrectly assumes that it need not run the extruder forward to restore working pressure. The to-be-printed G1 commands all seem to be very short in the failing G-Code files I’ve examined.

    Setting the reversal threshold to 0.0 should avoid triggering this error. I’ve verified that it produces correct G-Code for two parts that didn’t work before, but that’s not conclusive proof.

    I’ve looked into reversal.py and fixing (heck, finding) this error lies beyond my abilities.

    This is now Issue 175 on the ReplicatorG tracker…

  • Skeinforge Build Parameters

    The extrusion settings, more or less, kinda-sorta, for the latest objects:

    • Layer thickness 0.33 mm
    • Perimeter w/t = 1.75 = 0.58 mm
    • Fill w/t = 1.65 (or as needed)
    • Feed 40 mm/s
    • Flow 2 rev/min with geared stepper
    • Perimeter feed/flow 75% of normal (probably not needed)
    • First layer at 20% of normal feed & flow
    • 210 °C (some at 220 °C) Thermal Core
    • 120 °C build platform (lower at plate surface)
    • Reversal: 20 rev/min, 90 ms reverse & push-back (lower better?)
    • Fill: 2 extra shells, 3 solid surface layers, 0.25 solidity, 0.3 overlap
    • Thread sequence: Loops / perimeter / infill
    • Cool: slow down, minimum 15 sec/layer
    • Bottom / splodge / stretch disabled

    Wouldn’t it be great if you could export all that stuff to a text file in a readable format? The CSV files come close, but they’re not really meant for human consumption.

    Subject to revision, your mileage may vary, past performance is no indication of future yield, perfectly safe when used exactly as directed, shake before using, don’t touch that dial!

  • Adobe Reader Print Colors

    While printing up handouts for my talk at Cabin Fever, I finally tracked down why Adobe Reader was producing such crappy colors.

    The left is before and the right is after the fix, scanned at the same time with the same image adjustments:

    Oversaturated vs normal printing
    Oversaturated vs normal printing

    All of the print settings appeared correct (plain paper, 720 dpi, normal contrast, etc, etc), but Adobe Reader (and only Adobe Reader) looked like it was trying to print on vastly higher quality paper than I was using. Too much ink, too much contrast, generally useless results.

    The solution was, as always, trivial, after far too much fiddling around.

    In Reader’s Print dialog, there’s a button in the lower-left corner labeled Advanced. Clicky, then put a checkmark in the box that says Let printer determine colors.

    And then It Just Works.

    Equally puzzling: ask for 25 copies of a two-page document, check the Collate box, and you get 25 page 1, 25 page 2, then more page 1 starts coming out. I bet I’d get 25 x 25 sheets of paper by the time it gave up.

    I have no idea what’s going on, either.

    Memo to Self: verify that the box stays checked after updates.

  • Installing OpenSCAD on Arch Linux

    This was more tedious than it ought to be, but OpenSCAD now runs on my desktop box and uses OpenGL 2.2, courtesy of a not too obsolete nVidia GeForce 9400 dual-head card.

    OpenSCAD has a slew of pre-reqs, most of which were already installed. However, the openscad and cgal non-packages live in the Arch AUR collection, so they required manual twiddling to install.

    The pre-reqs:

    • cgal, which in turn requires cmake via pacman
    • opencsg

    The recommended PKGBUILD patch is easy enough to do by hand.

    The final build step takes ten minutes using both cores, but the final result uses OpenCSG the way it should.

    Oddly, the OpenSCAD rendering process for the few objects I’ve checked takes longer than on the laptop. Weird.

    This does not get the most recent build from the developers, but it’s close enough for my simple needs right now. The mailing list archive is invaluable.

    Then there was the laptop saga. Maybe the reason the laptop is faster is that it’s not actually using OpenCSG at all.

  • Thermocouple Calibration: Linear Regression

    With the thermistors nestled all snug in their wells, I turned on the heat and recorded the temperatures. I picked currents roughly corresponding to the wattages shown, only realizing after the fact that I’d been doing the calculation for the 5 Ω Thing-O-Matic resistors, not the 6 Ω resistor I was actually using. Doesn’t matter, as the numbers depend only on the temperatures, not the wattage.

    This would be significantly easier if I had a thermocouple with a known-good calibration, but I don’t. Assuming that the real temperature lies somewhere near the average of the six measurements is the best I can do, so … onward!

    Plotting the data against the average at each measurement produces a cheerful upward-and-to-the-right graph:

    Data vs Ensemble Average
    Data vs Ensemble Average

    So the thermocouples seem reasonably consistent.

    Plotting the difference between each measurement and the average of all the measurements at that data point produces this disconcertingly jaggy result:

    Difference from Ensemble Average
    Difference from Ensemble Average

    The TOM thermocouple seems, um, different, which is odd, because the MAX6675 converts directly from thermocouple voltage to digital output with no intervening software. It’s not clear what’s going on; I don’t know if the bead was slightly out of its well or if that’s an actual calibration difference. I’ll check it later, but for now I will simply run with the measurements.

    Eliminating the TOM data from the average produces a better clustering of the remaining five readings, with the TOM being even further off. The regression lines show the least-squares fit to each set of points, which look pretty good:

    Difference from Average without TOM
    Difference from Average without TOM

    Those regression lines give the offset and slope of the best-fit line that goes from the average reading to the actual reading, but I really need an equation from the actual reading for each thermocouple to the combined average. Rather than producing half a dozen graphs, I applied the spreadsheet’s SLOPE() and INTERCEPT() functions with the average temperature as Y and the measured temperature as X.

    That produced this table:

                        TOM     MPJA  Craftsman A  Craftsman B   Fluke T1  Fluke T2
    M = slope        1.0534   0.5434       0.5551       0.5539     1.0112    1.0154
    B = intercept   -1.6073 -15.3703     -19.4186     -16.9981    -0.7421   -0.3906
    

    And then, given a reading from any of the thermocouples, converting that value to the average requires plugging the appropriate values from that table into good old

    • y = mx + b

    For example, converting the Fluke 52 T1 readings produces this table of values. The Adjusted column shows the result of that equation and the Delta Avg column gives the difference from the average temperature (not shown here) for that reading.

    Fluke T1    Adjusted   Delta Avg   Max Abs Err
    21.0        20.5        -0.4          0.78
    29.0        28.6        -0.3
    34.8        34.4        -0.3
    45.5        45.3        -0.2
    50.1        49.9         0.0
    52.0        51.8         0.2
    69.3        69.3         0.3
    76.4        76.5         0.4
    78.9        79.0         0.6
    107.9       108.4         0.2
    112.3       112.8         0.4
    117.5       118.1         0.3
    127.8       128.5        -0.2
    133.2       134.0         0.1
    136.6       137.4         0.1
    138.1       138.9         0.1
    146.4       147.3        -0.4
    155.8       156.8        -0.8
    

    The Max Avg Error (the largest value of the absolute difference from the average temperature at each point) after correction is 0.78 °C for this set. The others are less than that, with the exception of the TOM thermocouple, which differs by 1.81 °C.

    So now I can make a whole bunch of temperature readings, adjust them to the same “standard”, and be off by (generally) less than 1 °C. That’s much better than the 10 °C of the unadjusted readings and seems entirely close enough for what I need…

  • Faking OpenGL 2.0 on Intel i945 Hardware

    OpenSCAD grumps about not finding OpenGL 2.0 whenever it starts up on my ancient laptop, which is tedious: that situation just isn’t going to change. Not a fatal error, although I do wonder what the OpenCSG rendering would look like.

    Anyhow, a bit of rummaging turns up a hack that’ll cause OpenSCAD to STFU and just start up. That doesn’t make OpenCSG work, which is pretty much not a problem for my simple needs.

    On Ubuntu-flavored distros, install driconf, then activate two options (in the Performance and Debugging tabs, respectively):

    • Enable limited ARB_fragment_shader support on 915/945
    • Enable stub ARB_occlusion_query support on 915/945

    And then It Just Works…

     

     

  • Cabin Fever Tchotchke: Engraved Dog Tag

    Once again I’m planning to attend the Cabin Fever Expo in York; my shop assistant says this year she won’t barf in the kitchen sink Thursday evening just before bedtime…

    If I’m going to haul a Sherline CNC setup that far and spend all day talking machining, I must have some tchotchkes / swag to talk about. We figured a small plastic dog tag with relevant URLs would be appropriate.

    Cabin Fever Dog Tag
    Cabin Fever Dog Tag

    I modeled the tag after my father’s WWII tag, including the mysterious notch. The rounded ends actually have three curves: two small fairing arcs blend the sides into the end cap.

    The G-Code routine figures out all the coordinates and suchlike from some basic physical measurements & guesstimates, so tweaking the geometery is pretty straightforward. There was a blizzard going on while I wrote it: a fine day to spend indoors hacking code.

    My assistant fired up Inkscape, laid out the text, figured out how to coerce G-Code out of Inkscape using the cnc-club.ru extension, then aligned it properly with the center of the chain hole as the origin on the right side. My routine calls the text G-Code file as a subroutine.

    The extension’s header and footer files wrap EMC2’s SUB / ENDSUB syntactic sugar around the main file. The default files include an M2 that kills off the program; took a while to track that one down.

    The header file:

    O<dogtagtext> SUB
    

    And the matching footer file:

    O<dogtagtext> ENDSUB
    

    The Inkscape-to-gcode instructions come out with absolute coordinates relative to the origin you define when you create the layout. The nested loops in my wrapper slap a G55 coordinate offset atop each label in turn, then call the subroutine.

    The result is pretty slick:

    Screenshot: AXIS Dog Tags
    Screenshot: AXIS Dog Tags

    I carved out that proof-of-concept label atop double-sided adhesive tape, but peeling off the goo is a real pain; a 2×3 array will be much worse. I’d rather do that than figure out how to clamp the fool things to the sacrificial plate, though.

    The engraving is 0.2 mm deep with a Dremel 30 degree tool. My shop assistant describes it as “disturbing” the acrylic, not actually engraving a channel. This isn’t entirely a Bad Thing, as the font isn’t quite a stick font and the outline of each character mushes together. We must fiddle with the font a bit more; she favors a boldified OCR-A look.

    Some lessons:

    • The Kate G-Code syntax highlighter isn’t down with EMC2’s dialect
    • Be very sure you touch off the workpiece origin in G54, not G55
    • Xylene doesn’t bother acrylic and works fine on tape adhesive
    • Symlinks aimed across an NFS link work fine in ~/emc2/nc_files/
    • That 2×3 array may be too big for the Sherline’s tooling plate
    • Tool length probing FTW!

    The G-Code:

    (Cabin Fever 2011 Dogtag)
    (Ed Nisley - KE4ZNU - December 2010)
    (Origin at center of chain hole near right side)
    (Stock held down with double-stick tape)
    
    (--------------------)
    (Flow Control)
    
    #<_DoText>      = 1
    #<_DoDrill>     = 1
    #<_DoMill>      = 1
    
    ( Sizes and Shapes)
    
    (-- Tag array layout)
    
    #<_NumTagsX>    = 3                         (number of tags along X axis)
    #<_NumTagsY>    = 2                         ( ... Y axis)
    
    #<_TagSpaceX>   = 60                        (center-to-center along X axis)
    #<_TagSpaceY>   = 35                        ( ... Y axis)
    
    (-- Tag Dimensions)
    
    #<_TagSizeX>    = 50.8                      (2.0 inches in WWII!)
    #<_TagSizeY>    = 28.6                      (1-1/8 inches)
    #<_TagSizeZ>    = 2.0
    
    #<_HoleOffsetX> = 4.0                       (hole center to right-side tag edge)
    
    #<_NotchSizeX>      = 3.5                   (locating notch depth from far left edge)
    #<_NotchCtrY>       = 5.0                   (locating notch from Y=0)
    
    #<_NotchAngleBot>   = 30                    (lower angle in notch)
    #<_NotchAngleTop>   = 45                    (upper angle in notch)
    
    (-- Fairing Curve Dimensions as offsets from end arc center)
    
    #<_EndFairR>    = [0.68 * #<_TagSizeY>]
    #<_CornerFairR> = [0.25 * #<_TagSizeY>]
    
    #<_PCRadius>    = [#<_EndFairR> - #<_CornerFairR>]
    #<_PCY>         = [[#<_TagSizeY> / 2] - #<_CornerFairR>]
    #<_PCTheta>     = ASIN [#<_PCY> / #<_PCRadius>]
    #<_PCX>         = [#<_PCRadius> * COS [#<_PCTheta>]]
    
    #<_P1Y>         = [#<_TagSizeY> / 2]                    (top / bottom endpoint)
    #<_P1X>         = #<_PCX>
    
    #<_P2X>         = [#<_EndFairR> * COS [#<_PCTheta>]]
    #<_P2Y>         = [#<_EndFairR> * SIN [#<_PCTheta>]]
    
    (-- Tooling)
    
    #<_TraverseZ>   = 1.0                       (safe clearance above workpiece)
    
    #<_DrillDia>    = 3.2                       (drill for hole and notch)
    #<_DrillNum>    = 1                         ( ... tool number)
    #<_DrillRadius> = [#<_DrillDia> / 2]
    #<_DrillFeed>   = 200                       (drill feed for holes)
    #<_DrillRPM>    = 3000
    
    #<_MillDia>     = 3.2                       (mill for outline)
    #<_MillNum>     = 1                         ( ... tool number)
    #<_MillRadius> = [#<_MillDia> / 2]
    #<_MillFeed>    = 150                       (tool feed for outlines)
    #<_MillRPM>     = 5000
    
    #<_TextDia>     = 0.1                       (engraving tool)
    #<_TextNum>     = 1
    #<_TextFeed>    = 600                       (tool feed for engraving)
    #<_TextRPM>     = 10000
    
    (-- Useful calculated values)
    
    #<_TagRightX>   = #<_HoleOffsetX>           (extreme limits of tag in X)
    #<_TagLeftX>    = [#<_TagRightX> - #<_TagSizeX>]
    
    #<_EndFairRtX>  = [#<_TagRightX> - #<_EndFairR>]
    #<_EndFairLfX>  = [#<_TagLeftX> + #<_EndFairR>]
    
    #<_NotchCtrX>   = [#<_TagLeftX> + #<_NotchSizeX> - #<_DrillRadius>]
    
    (--------------------)
    (--------------------)
    ( Initialize first tool length at probe switch)
    (    Assumes G59.3 is still in machine units, returns in G54)
    ( ** Must set these constants to match G20 / G21 condition!)
    
    #<_Probe_Speed>     = 400            (set for something sensible in mm or inch)
    #<_Probe_Retract>   =   1            (ditto)
    
    O<Probe_Tool> SUB
    
    G49                     (clear tool length compensation)
    G30                     (move above probe switch)
    G59.3                   (coord system 9)
    
    G38.2 Z0 F#<_Probe_Speed>           (trip switch on the way down)
    
    G0 Z[#5063 + #<_Probe_Retract>]     (back off the switch)
    
    G38.2 Z0 F[#<_Probe_Speed> / 10]    (trip switch slowly)
    
    #<_ToolZ> = #5063                    (save new tool length)
    
    G43.1 Z[#<_ToolZ> - #<_ToolRefZ>]    (set new length)
    
    G54                     (coord system 0)
    G30                     (return to safe level)
    
    O<Probe_Tool> ENDSUB
    
    (-------------------)
    (-- Initialize first tool length at probe switch)
    
    O<Probe_Init> SUB
    
    #<_ToolRefZ> = 0.0      (set up for first call)
    
    O<Probe_Tool> CALL
    
    #<_ToolRefZ> = #5063    (save trip point)
    
    G43.1 Z0                (tool entered at Z=0, so set it there)
    
    O<Probe_Init> ENDSUB
    
    (--------------------)
    (Start machining)
    
    G40 G49 G54 G80 G90 G94 G97 G98     (reset many things)
    
    G21                                 (metric!)
    
    (msg,Verify G30.1 position in G54 above tool change switch)
    M0
    (msg,Verify XYZ=0 touched off at left front tag hole center on surface)
    M0
    
    O<Probe_Init> CALL
    T0 M6                           (clear the probe tool)
    
    (-- Engrave Text)
    
    O<DoText> IF [#<_DoText>]
    
    (msg,Insert engraving tool)
    T#<_TextNum> M6         (load engraving tool)
    O<Probe_Tool> CALL
    
    F#<_TextFeed>
    S#<_TextRPM>
    
    (debug,Set spindle to #<_TextRPM>)
    M0
    
    G0 X0 Y0                (get safely to first tag)
    G0 Z#<_TraverseZ>       (to working level)
    
    G10 L20 P2 X0 Y0 Z#<_TraverseZ>         (set G55 origin to 0,0 at this point)
    G55                                     (activate G55 coordinates)
    
    O3000 REPEAT [#<_NumTagsX>]
    
    O3100 REPEAT [#<_NumTagsY>]
    
    O<dogtagtext> CALL
    
    G0 X0 Y0
    G10 L20 P2 Y[0 - #<_TagSpaceY>]         (set Y orgin relative to next tag in +Y direction)
    
    O3100 ENDREPEAT
    
    G10 L20 P2 X[0 - #<_TagSpaceX>] Y[[#<_NumTagsY> - 1] * #<_TagSpaceY>] (next to +X, Y to front)
    
    O3000 ENDREPEAT
    
    G54                                     (bail out of G55 coordinates)
    
    (-- Drill holes)
    
    O<DoDrill> IF [#<_DoDrill>]
    
    T0 M6
    (msg,Insert drill)
    T#<_DrillNum> M6
    O<Probe_Tool> CALL
    
    F#<_DrillFeed>
    S#<_DrillRPM>
    
    #<_DrillZ> = [0 - #<_TagSizeZ> - #<_DrillRadius>]
    
    (debug,Set spindle to #<_DrillRPM>)
    M0
    
    G0 X0 Y0                (get safely to first tag)
    G0 Z#<_TraverseZ>       (to working level)
    
    #<IndexX> = 0
    O1000 DO
    
    #<IndexY> = 0
    O1100 DO
    
    #<TagOriginX> = [#<IndexX> * #<_TagSpaceX>]
    #<TagOriginY> = [#<IndexY> * #<_TagSpaceY>]
    
    G81 X#<TagOriginX> Y#<TagOriginY> Z#<_DrillZ> R#<_TraverseZ>
    G81 X[#<TagOriginX> + #<_NotchCtrX>] Y[#<TagOriginY> + #<_NotchCtrY>] Z#<_DrillZ> R#<_TraverseZ>
    
    #<IndexY> = [#<IndexY> + 1]
    O1100 WHILE [#<IndexY> LT #<_NumTagsY>]
    
    #<IndexX> = [#<IndexX> + 1]
    O1000 WHILE [#<IndexX> LT #<_NumTagsX>]
    
    G30     (go home)
    
    O<DoDrill> ENDIF
    
    (-- Machine outlines)
    
    O<DoMill> IF [#<_DoMill>]
    
    T0 M6                   (eject drill)
    (msg,Insert end mill)
    T#<_MillNum> M6         (load mill)
    O<Probe_Tool> CALL
    
    F#<_MillFeed>
    S#<_MillRPM>
    
    (debug,Set spindle to #<_MillRPM>)
    M0
    
    G0 X0 Y0                (get safely to first tag)
    G0 Z#<_TraverseZ>       (to working level)
    
    G10 L20 P2 X0 Y0 Z#<_TraverseZ>         (set G55 origin to 0,0 at this point)
    G55                                     (activate G55 coordinates)
    
    O2000 REPEAT [#<_NumTagsX>]
    
    O2100 REPEAT [#<_NumTagsY>]
    
    G0 X[#<_NotchCtrX>] Y[#<_NotchCtrY>]     (get to center of notch hole)
    G0 Z[0 - #<_TagSizeZ>]                      (down to cutting level)
    
    G91                                         (relative coordinate for notch cutting)
    G1 X[0 - #<_NotchSizeX>] Y[0 -  #<_NotchSizeX> * TAN [#<_NotchAngleBot>]]
    G1 X[0 + #<_NotchSizeX>] Y[0 +  #<_NotchSizeX> * TAN [#<_NotchAngleBot>]]
    G1 X[0 - #<_NotchSizeX>] Y[0 +  #<_NotchSizeX> * TAN [#<_NotchAngleTop>]]
    G90                                         (back to abs coords)
    
    G42.1 D#<_MillDia>                          (cutter comp to right)
    G1 X[#<_TagLeftX>] Y0                       (comp entry move to tip of left endcap)
    
    G3 X[#<_EndFairLfX> - #<_P2X>] Y[0 - #<_P2Y>] I[#<_EndFairR>] J0    (left endcap front half)
    
    G3 X[#<_EndFairLfX> - #<_P1X>] Y[0 - #<_P1Y>] I[#<_P2X> - #<_PCX>] J[#<_P2Y> - #<_PCY>]
    
    G1 X[#<_EndFairRtX> + #<_P1X>]                                      (front edge)
    
    G3 X[#<_EndFairRtX> + #<_P2X>] Y[0 - #<_P2Y>] I0 J[#<_CornerFairR>]
    
    G3 X[#<_EndFairRtX> + #<_P2X>] Y[#<_P2Y>] I[0 - #<_P2X>] J[#<_P2Y>]    (right endcap)
    
    G3 X[#<_EndFairRtX> + #<_P1X>] Y[#<_P1Y>] I[#<_PCX> - #<_P2X>] J[#<_PCY> - #<_P2Y>]
    
    G1 X[#<_EndFairLfX> - #<_P1X>]                                      (rear edge)
    
    G3 X[#<_EndFairLfX> - #<_P2X>] Y[#<_P2Y>] I0 J[0 - #<_CornerFairR>]
    
    G3 X[#<_EndFairLfX> - #<_P2X>] Y[0 - #<_P2Y>] I[#<_P2X>] J[0 - #<_P2Y>]    (left endcap complete)
    
    G0 Z#<_TraverseZ>
    
    G40
    
    G0 X0 Y0
    G10 L20 P2 Y[0 - #<_TagSpaceY>]         (set Y orgin relative to next tag in +Y direction)
    
    O2100 ENDREPEAT
    
    G10 L20 P2 X[0 - #<_TagSpaceX>] Y[[#<_NumTagsY> - 1] * #<_TagSpaceY>] (next to +X, Y to front)
    
    O2000 ENDREPEAT
    
    G54                                     (bail out of G55 coordinates)
    
    G30         (go home)
    
    O<DoMill> ENDIF
    
    M2
    
    

    The doodles leading to the equations:

    Dog Tag Geometry Doodles
    Dog Tag Geometry Doodles

    We’ll see you there!