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

  • Tek Circuit Computer: Acrylic Cursor Hairline

    A slide rule needs a cursor with a hairline to align numbers on its scales:

    Tek Circuit Computer - cursor hairline
    Tek Circuit Computer – cursor hairline

    The GCMC code generating the hairline is basically a move scratching one line into the surface with the diamond bit:

      feedrate(ScaleSpeed);
    
      goto([-,-,TravelZ]);
    
      repeat(2) {
        goto([DeckTopOD/2 - 3*ScaleHeight,0,-]);
        move([-,-,EngraveZ]);
        move([DeckBottomOD/2 + ScaleHeight,0,-]);
        goto([-,-,TravelZ]);
      }
    

    Two passes make the scratch deep enough to hold engraving crayon / lacquer / ink, without making it much wider. Laser engraving would surely work better.

    In lieu of actually milling the cursor, this code scratches the perimeter:

      local dr = DeckBottomOD/2;
      local hr = CursorHubOD/2;
      local a = atan(hr - CursorTipWidth/2,dr);   // rough & ready approximation
    
      local p0 = hr * [sin(a),cos(a),-];          // upper tangent point on hub
    
      local c1 = [dr - CursorTipRadius,CursorTipWidth/2 - CursorTipRadius*cos(a),-];
      local p1 = c1 + [CursorTipRadius*sin(a),CursorTipRadius*cos(a),-];
    
      local p2 = c1 + [CursorTipRadius,0,-];      // around tip radius
    
      feedrate(KnifeSpeed);
    
      goto([-,-,TravelZ]);
      goto([-hr,0,-]);
      move([-,-,EngraveZ]);
    
      repeat(3) {
        arc_cw(p0,hr);
        move(p1);
        arc_cw(p2,CursorTipRadius);
    
        move([p2.x,-p2.y,-]);
        arc_cw([p1.x,-p1.y,-],CursorTipRadius);
        move([p0.x,-p0.y,-]);
        arc_cw([-hr,0,-],hr);
      }

    Three passes makes it deep enough to snap along the line:

    Tektronix Circuit Computer - cursor outline
    Tektronix Circuit Computer – cursor outline

    If you look closely, though, you’ll find a little divot over on the left along the bottom edge, so I really must machine the thing.

    Were I to go into production, I’d have to figure out a fixture, but I think I can just clamp a rough-cut acrylic rectangle to the Sherline’s table, mill half the perimeter, re-clamp without moving anything, then mill the other half.

    Subtractive machining is such a bother!

    The pivot holding the cursor and decks together is a “Chicago screw“, a.k.a. a “sex bolt“. I am not making this up.

  • Windows-free BIOS Update

    A new-to-me Dell Optiplex 9020 needed a BIOS update, which, as always, arrives in a Windows / DOS EXE file. Because I’d already swapped in an SSD and installed Manjaro, I had to (re-)discover how to put the EXE file on a bootable DOS USB stick.

    The least horrible way seemed to be perverting a known-good FreeDOS installation image:

    sha256sum FD12FULL.zip 
    fd353f20f509722e8b73686918995db2cd03637fa68c32e30caaca70ff94c6d2  FD12FULL.zip

    Unzip it to get the USB image file, then find the partition offset:

    fdisk -l FD12FULL.img
    Disk FD12FULL.img: 512 MiB, 536870912 bytes, 1048576 sectors
    Units: sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disklabel type: dos
    Disk identifier: 0x00000000
    
    Device        Boot Start     End Sectors   Size Id Type
    FD12FULL.img1 *       63 1048319 1048257 511.9M  6 FAT16

    Mount the partition as a loop device:

    sudo mount -o loop,offset=$((63*512)),uid=ed FD12FULL.img /mnt/loop

    See how much space is left:

    df -h /mnt/loop
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/loop0      512M  425M   87M  84% /mnt/loop

    The image file is 512 MB and has 87 MB available. The BIOS file is 9.5 MB, so copy the file to the “drive”:

    cp O9020A25.exe /mnt/loop

    Which knocks the available space down by about what you’d expect:

    df -h /mnt/loop
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/loop0      512M  435M   78M  85% /mnt/loop

    Unmount the image “drive”:

    sudo umount /mnt/loop

    Copy the image file to a USB stick:

    sudo dcfldd status=progress bs=1M if=FD12FULL.img of=/dev/sdg
    512 blocks (512Mb) written.
    512+0 records in
    512+0 records out

    Pop the USB stick in the Optiplex, set the BIOS to boot from “Legacy” ROMs, whack F12 during the reboot, pick the USB stick from the list, and It Just Works™:

    BIOS Update screen
    BIOS Update screen

    We have a couple of other 9020s around that need the same treatment, so the effort won’t go to waste.

  • GCMC: Circular Slide Rule Scales

    The Tektronix Circuit Computer, being a specialized circular slide rule, requires logarithmic scales bent around arcs:

    Scale Tick Layout - Bottom Deck
    Scale Tick Layout – Bottom Deck

    Each decade spans 18°, except for the FL scale’s 36° span to extract the square root of the LC product:

    FL = 1 / (2π · sqrt(LC))

    The tick marks can point inward or outward from their baseline radius, with corresponding scale labels reading either inward or outward.

    There being no (easy) way to algorithmically set the tick lengths, I used a (pair of) tables (a.k.a. vector lists):

    TickScaleNarrow = {
      [1.0,TickMajor],
      [1.1,TickMinor],[1.2,TickMinor],[1.3,TickMinor],[1.4,TickMinor],
      [1.5,TickMid],
      [1.6,TickMinor],[1.7,TickMinor],[1.8,TickMinor],[1.9,TickMinor],
      [2.0,TickMajor],
      [2.2,TickMinor],[2.4,TickMinor],[2.6,TickMinor],[2.8,TickMinor],
      [3.0,TickMajor],
    … and so on …

    The first number in each vector is the tick value in the decade, the log of which corresponds to its angular position. The second gives its length, with three constants matching up to the actual lengths on the Tek scales.

    The Circuit Computer labels only three ticks within each decade in the familiar (to EE bears, anyhow) 1, 2, 5 sequence. Their logs are 0.0, 0.3, and 0.7, spacing them neatly at the 1/3 decade points.

    Pop quiz: If you wanted to label two evenly spaced ticks per decade, you’d mark 1 and …

    Generating the L (inductance) scale on the bottom deck goes like this:

      Radius = DeckRad - ScaleSpace;
    
      MinLog = -9;
      MaxLog = 6;
      Arc = -ScaleArc;
    
      dec = 1;
      offset = 0deg;
      for (logval = MinLog; logval < MaxLog; logval++) {
        a = offset + logval * Arc;
        DrawTicks(Radius,TickScaleNarrow,OUTWARD,a,Arc,dec,INWARD,FALSE);
        dec = (dec == 100) ? 1 : 10 * dec;
      }
    
      a = offset + MaxLog * Arc;
      DrawTicks(Radius,TickScaleNarrow,OUTWARD,a,Arc,100,INWARD,TRUE);

    The L scale covers 1 nH to 1 MH (!), as set by the MinLog and MaxLog values. Arc sets the angular size of each decade from ScaleArc, with the negative sign indicating the values increase in the clockwise direction.

    The first decade starts with a tick labeled 1, so dec = 1. The next decade has dec = 10 and the third has dec = 100. Maybe I should have used the log values 0, 1, and 2, but that seemed too intricate.

    The angular offset is zero because this is the outermost scale, so 1.0 H will be at 0° (the picture is rotated about half a turns, so you’ll find it off to the left). All other scales on the deck have a nonzero offset to put their unit tick at the proper angle with respect to this one.

    The scales have legends for each group of three decades, positioned in the middle of the group:

      logval = MinLog + 1.5;
      a = offset + logval * Arc;
      ArcLegend("nH - nanohenry  x10^-9",r,a,INWARD);
    
      logval += 3;
      a = offset + logval * Arc;
      ArcLegend("μH - microhenry  x10^-6",r,a,INWARD);

    I wish there were a clean way to draw exponents, as the GCMC Hershey font does not include superscripts, but the characters already live at the small end of what’s do-able with a ballpoint pen cartridge. Engraving will surely work better, but stylin’ exponents are definitely in the nature of fine tuning.

    With all that in hand, the scales look just like they should:

    Tektronix Circuit Computer - Bottom Deck - scale detail
    Tektronix Circuit Computer – Bottom Deck – scale detail

    The GCMC source code as a GitHub Gist:

    //—-
    // Define tick layout for scales
    // Numeric value for scale, corresponding tick length
    TickScaleNarrow = {
    [1.0,TickMajor],
    [1.1,TickMinor],[1.2,TickMinor],[1.3,TickMinor],[1.4,TickMinor],
    [1.5,TickMid],
    [1.6,TickMinor],[1.7,TickMinor],[1.8,TickMinor],[1.9,TickMinor],
    [2.0,TickMajor],
    [2.2,TickMinor],[2.4,TickMinor],[2.6,TickMinor],[2.8,TickMinor],
    [3.0,TickMajor],
    [3.2,TickMinor],[3.4,TickMinor],[3.6,TickMinor],[3.8,TickMinor],
    [4.0,TickMajor],
    [4.5,TickMinor],
    [5.0,TickMajor],
    [5.5,TickMinor],
    [6.0,TickMajor],
    [6.5,TickMinor],
    [7.0,TickMajor],
    [7.5,TickMinor],
    [8.0,TickMajor],
    [8.5,TickMinor],
    [9.0,TickMajor],
    [9.5,TickMinor]
    };
    TickScaleWide = {
    [1.0,TickMajor],
    [1.1,TickMinor],[1.2,TickMinor],[1.3,TickMinor],[1.4,TickMinor],
    [1.5,TickMid],
    [1.6,TickMinor],[1.7,TickMinor],[1.8,TickMinor],[1.9,TickMinor],
    [2.0,TickMajor],
    [2.1,TickMinor],[2.2,TickMinor],[2.3,TickMinor],[2.4,TickMinor],
    [2.5,TickMid],
    [2.6,TickMinor],[2.7,TickMinor],[2.8,TickMinor],[2.9,TickMinor],
    [3.0,TickMajor],
    [3.2,TickMinor],[3.4,TickMinor],[3.6,TickMinor],[3.8,TickMinor],
    [4.0,TickMajor],
    [4.2,TickMinor],[4.4,TickMinor],[4.6,TickMinor],[4.8,TickMinor],
    [5.0,TickMajor],
    [5.5,TickMinor],
    [6.0,TickMajor],
    [6.5,TickMinor],
    [7.0,TickMajor],
    [7.5,TickMinor],
    [8.0,TickMajor],
    [8.5,TickMinor],
    [9.0,TickMajor],
    [9.5,TickMinor]
    };
    TickLabels = [1,2,5]; // labels only these ticks, must be integers
    /—–
    // Draw a decade of ticks & labels
    // ArcLength > 0 = CCW, < 0 = CW
    // UnitOnly forces just the unit tick, so as to allow creating the last tick of the scale
    function DrawTicks(Radius,TickMarks,TickOrient,UnitAngle,ArcLength,Decade,LabelOrient,UnitOnly) {
    feedrate(ScaleSpeed);
    local a,r0,r1,p0,p1;
    if (Decade == 1 || UnitOnly) { // draw unit marker
    a = UnitAngle;
    r0 = Radius + TickOrient * (TickMajor + 2*TickGap + ScaleTextSize.y);
    p0 = r0 * [cos(a),sin(a)];
    r1 = Radius + TickOrient * (ScaleSpace – 2*TickGap);
    p1 = r1 * [cos(a),sin(a)];
    goto(p0);
    move([-,-,EngraveZ]);
    move(p1);
    goto([-,-,TravelZ]);
    }
    local ticklist = UnitOnly ? {TickMarks[0]} : TickMarks;
    local tick;
    foreach(ticklist; tick) {
    a = UnitAngle + ArcLength * log10(tick[0]);
    p0 = Radius * [cos(a), sin(a)];
    p1 = (Radius + TickOrient*tick[1]) * [cos(a), sin(a)];
    goto(p0);
    move([-,-,EngraveZ]);
    move(p1);
    goto([-,-,TravelZ]);
    }
    feedrate(TextSpeed); // draw scale values
    local lrad = Radius + TickOrient * (TickMajor + TickGap);
    if (TickOrient == INWARD) {
    if (LabelOrient == INWARD) {
    lrad -= ScaleTextSize.y; // inward ticks + inward labels = offset inward
    }
    }
    else {
    if (LabelOrient == OUTWARD) {
    lrad += ScaleTextSize.y; // outward ticks + outward labels = offset outward
    }
    }
    ticklist = UnitOnly ? [TickLabels[0]] : TickLabels;
    local ltext,lpath,tpa;
    foreach(ticklist; tick) {
    ltext = to_string(Decade * to_int(tick));
    lpath = scale(typeset(ltext,TextFont),ScaleTextSize);
    a = UnitAngle + ArcLength * log10(tick);
    tpa = ArcText(lpath,[0mm,0mm],lrad,a,TEXT_CENTERED,LabelOrient);
    engrave(tpa,TravelZ,EngraveZ);
    }
    }

  • GCMC Radial Text

    The Tektronix Circuit Computer needs text along radial lines:

    Tek CC - original RC arrow layout
    Tek CC – original RC arrow layout

    Fortunately, this doesn’t require nearly as much effort as the text-on-arcs code, because GCMC includes functions to rotate paths around the origin:

    return rotate_xy(TextPath,Angle) + CenterPt;

    The only trick is figuring out how to handle the justification, given the overall path length:

      local pl = TextPath[-1].x;
    
      local ji = (Justify == TEXT_LEFT)     ? 0mm :
                 (Justify == TEXT_CENTERED) ? -pl/2 :
                 (Justify == TEXT_RIGHT)    ? -pl :
                 0mm;

    A testcase showed it worked:

    Radial text testcase
    Radial text testcase

    With that in hand, I took the liberty of slightly simplifying the original Tek layout:

    Tek CC - radial text example
    Tek CC – radial text example

    If lives depended on it, one could duplicate the Tek layout, but they don’t and I didn’t. Fancy typography isn’t a GCMC thing.

    And, yeah, laser printing is way crisper than a pen drawing.

    The GCMC source code as a GitHub Gist:

    comment("RadialText test");
    ctr = [0mm,0mm];
    r = 20mm;
    a = 0deg;
    tp = scale(typeset("Left Inward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_LEFT,INWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    tp = scale(typeset("Left Outward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_LEFT,OUTWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    a = 90deg;
    tp = scale(typeset("Right Inward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_RIGHT,INWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    tp = scale(typeset("Right Outward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_RIGHT,OUTWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    a = 180deg;
    tp = scale(typeset("Center Inward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_CENTERED,INWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    tp = scale(typeset("Center Outward",TextFont),LegendTextSize);
    tpr = RadialText(tp,ctr,r,a,TEXT_CENTERED,OUTWARD);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    a = 270deg;
    RadialLegend("Offset to radius",ctr,r,a,TEXT_CENTERED,INWARD,-0.5);
    goto(ctr);
    move([0,-2*r,EngraveZ]);
    goto([r,0mm,-]);
    circle_cw(ctr);
    //—–
    // Write text on a radial line
    function RadialText(TextPath,CenterPt,Radius,Angle,Justify,Orient) {
    local pl = TextPath[-1].x; // path length
    local ji = (Justify == TEXT_LEFT) ? 0mm : // justification indent
    (Justify == TEXT_CENTERED) ? -pl/2 :
    (Justify == TEXT_RIGHT) ? -pl :
    0mm;
    if (Orient == INWARD) {
    TextPath = rotate_xy(TextPath,180deg);
    ji = -ji;
    }
    TextPath += [Radius + ji,0mm];
    return rotate_xy(TextPath,Angle) + CenterPt;
    }
    //—–
    // Draw a radial legend
    // Offset in units of char height: 0 = baseline on radius, +/- = above/below
    function RadialLegend(Text,Center,Radius,Angle,Justify,Orient,Offset) {
    local tp = scale(typeset(Text,TextFont),LegendTextSize) + [0mm,Offset * LegendTextSize.y];
    local tpr = RadialText(tp,Center,Radius,Angle,Justify,Orient);
    feedrate(TextSpeed);
    engrave(tpr,TravelZ,EngraveZ);
    }

  • GCMC Text on Arcs: Improved Version

    The Tektronix Circuit Computer scale annotations read both inward (from the center) and outward (from the rim):

    Text on Arcs - orientation
    Text on Arcs – orientation

    It’s surprisingly difficult (for me, anyhow) to see the middle FL Scale as reading upside-down, rather than mirror-image backwards.

    This turned into a rewrite of the the read-outward annotation code I used for the vacuum tube reflectors. Eventually the justification and orientation options came out right:

    Text-on-arcs example
    Text-on-arcs example

    The text baseline sits at the specified radius from the center point, regardless of its orientation, so you must offset the text path by half its height in the proper direction before handing it to the ArcText function.

    The testcase shows the invocation ritual:

        ctr = [0mm,0mm];
    
        tp = scale(typeset("Right Inward",TextFont),ScaleTextSize);
        tpa = ArcText(tp,ctr,30mm,45deg,TEXT_RIGHT,INWARD);
        engrave(tpa,TravelZ,EngraveZ);
        tp = scale(typeset("Right Outward",TextFont),ScaleTextSize);
        tpa = ArcText(tp,ctr,30mm,45deg,TEXT_RIGHT,OUTWARD);
        engrave(tpa,TravelZ,EngraveZ);
    
        tp = scale(typeset("Center Inward",TextFont),ScaleTextSize);
        tpa = ArcText(tp,ctr,20mm,45deg,TEXT_CENTERED,INWARD);
        engrave(tpa,TravelZ,EngraveZ);
        tp = scale(typeset("Center Outward",TextFont),ScaleTextSize);
        tpa = ArcText(tp,ctr,20mm,45deg,TEXT_CENTERED,OUTWARD);
        engrave(tpa,TravelZ,EngraveZ);
    
        tp = scale(typeset("Left Inward",TextFont),ScaleTextSize);
        tpa = ArcText(tp,ctr,10mm,45deg,TEXT_LEFT,INWARD);
        engrave(tpa,TravelZ,EngraveZ);
        tp = scale(typeset("Left Outward",TextFont),ScaleTextSize);
        tpa = ArcText(tp,ctr,10mm,45deg,TEXT_LEFT,OUTWARD);
        engrave(tpa,TravelZ,EngraveZ);
    
        goto([0mm,0mm,-]);
        move([40mm,40mm,-]);

    A utility function to draw scale legends stuffs some of that complexity into a bottle:

    function ArcLegend(Text,Radius,Angle,Orient) {
    
      local tp = scale(typeset(Text,TextFont),LegendTextSize);
      local tpa = ArcText(tp,[0mm,0mm],Radius,Angle,TEXT_CENTERED,Orient);
    
      feedrate(TextSpeed);
      engrave(tpa,TravelZ,EngraveZ);
    }

    Which means most of the text uses a simpler invocation:

      r = Radius + TickMajor + 2*TickGap + LegendTextSize.y;
    
      logval = MinLog + 1.5;
      a = offset + logval * Arc;
      ArcLegend("nH - nanohenry  x10^-9",r,a,INWARD);
    
      logval += 3;
      a = offset + logval * Arc;
      ArcLegend("μH - microhenry  x10^-6",r,a,INWARD);
    

    Arc determines the angular span of each decade, with positive values going counterclockwise. MinLog is the logarithm of the scale endpoint, so adding 1.5 puts the text angle one-and-a-half decades from MinLog and multiplying by Arc moves it in the right direction. The offset angle rotates the entire scale with respect to the 0° reference sticking out the X axis over on the right. The top picture has its 0° reference pointing north-northeast.

    The GCMC source code as a GitHub Gist:

    INWARD = -1; // text and tick alignment (used as integers)
    OUTWARD = 1;
    TEXT_LEFT = -1; // text justification
    TEXT_CENTERED = 0;
    TEXT_RIGHT = 1;
    //—–
    // Bend text around an arc
    function ArcText(TextPath,CenterPt,Radius,BaseAngle,Justify,Orient) {
    local pl = TextPath[-1].x; // path length
    local c = 2*pi()*Radius;
    local ta = to_deg(360 * pl / c); // subtended angle
    local ja = (Justify == TEXT_LEFT ? 0deg : // assume OUTWARD
    (Justify == TEXT_CENTERED) ? -ta / 2 :
    (Justify == TEXT_RIGHT) ? -ta :
    0deg);
    ja = BaseAngle + Orient*ja;
    local ArcPath = {};
    local pt,r,a;
    foreach(TextPath; pt) {
    if (!isundef(pt.x) && !isundef(pt.y) && isundef(pt.z)) { // XY motion, no Z
    r = (Orient == OUTWARD) ? Radius – pt.y : Radius + pt.y;
    a = Orient * 360deg * (pt.x / c) + ja;
    ArcPath += {[r*cos(a) + CenterPt.x, r*sin(a) + CenterPt.y,-]};
    }
    elif (isundef(pt.x) && isundef(pt.y) && !isundef(pt.z)) { // no XY, Z up/down
    ArcPath += {pt};
    }
    else {
    error("ArcText – Point is not pure XY or pure Z: " + to_string(pt));
    }
    }
    return ArcPath;
    }

    GCMC treats variables defined inside a function as local, unless they’re already defined in an enclosing scope, whereupon you overwrite the outer variables. This wasn’t a problem in my earlier programs, but I fired the footgun with nested functions using the same local / temporary variables. Now, I ruthlessly declare truly local variables as local, except when I don’t, for what seem good reasons at the time.

  • Among the Forgotten

    Spotted in a museum:

    Kiosk - Floppy Disk Seek Failure
    Kiosk – Floppy Disk Seek Failure

    It’s been quite a while since BIOS boot sequences started with the floppy drive. Combined with a CMOS backup battery failure, I’d say this poor PC has been chugging along for two decades.

    On another floor:

    Kiosk - Windows Updates
    Kiosk – Windows Updates

    Isolating a Windows kiosk from the Interwebs is an excellent design principle, but Windows Update really wants to phone home. The kiosk’s presentation ran Adobe Flash 10, so it’s been confined for maybe a decade.

    Looks like it’s time for another fundraising drive to replace the PCs with Raspberry Pi controllers. The real expense, of course, goes into rebuilding the presentations using whatever tech stack is trendy these days.

  • CNC 3018-Pro: Collet Pen Holder

    Along the same lines as the MPCNC pen holder, I now have one for the 3018:

    CNC3018 - Collet pen holder - assembled
    CNC3018 – Collet pen holder – assembled

    The body happened to be slightly longer than two LM12UU linear bearings stacked end-to-end, which I didn’t realize must be a constraint until I was pressing them into place:

    CNC 3018-Pro Collet Holder - LM12UU - solid model
    CNC 3018-Pro Collet Holder – LM12UU – solid model

    In the unlikely event I need another one, the code will sprout a max() function in the appropriate spot.

    Drilling the aluminum rod for the knurled ring produced a really nice chip:

    CNC3018 - Collet pen holder - drilling knurled ring
    CNC3018 – Collet pen holder – drilling knurled ring

    Yeah, a good drill will produce two chips, but I’ll take what I can get.

    There’s not much left of the original holder after turning it down to 8 mm so it fits inside the 12 mm rod:

    CNC3018 - Collet pen holder - turning collet OD
    CNC3018 – Collet pen holder – turning collet OD

    Confronted by so much shiny aluminum, I realized I didn’t need an 8 mm hole through the rod, so I cut off the collet shaft and drilled out the back end to clear the flanges on the ink tubes:

    CNC3018 - Collet pen holder - drilling out collet
    CNC3018 – Collet pen holder – drilling out collet

    I figured things would eventually go badly if I trimmed enough ink-filled crimps:

    Collet holder - pen cartridge locating flanges
    Collet holder – pen cartridge locating flanges

    The OpenSCAD source code as a GitHub Gist:

    // Collet Pen Holder in LM12UU linear bearings for CNC3018
    // Ed Nisley KE4ZNU – 2019-10-30
    Layout = "Build"; // [Build, Show, Base, Mount, Plate]
    /* [Hidden] */
    ThreadThick = 0.25; // [0.20, 0.25]
    ThreadWidth = 0.40; // [0.40, 0.40]
    /* [Hidden] */
    Protrusion = 0.1; // [0.01, 0.1]
    HoleWindage = 0.2;
    inch = 25.4;
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    ID = 0;
    OD = 1;
    LENGTH = 2;
    //- Adjust hole diameter to make the size come out right
    module PolyCyl(Dia,Height,ForceSides=0) { // based on nophead's polyholes
    Sides = (ForceSides != 0) ? ForceSides : (ceil(Dia) + 2);
    FixDia = Dia / cos(180/Sides);
    cylinder(r=(FixDia + HoleWindage)/2,h=Height,$fn=Sides);
    }
    //- Dimensions
    PenOD = 3.5; // pen cartridge diameter
    Bearing = [12.0,21.0,30.0]; // linear bearing body
    SpringSeat = [0.56,10.0,3*ThreadThick]; // wire = ID, coil = OD, seat depth = length
    WallThick = 4.0; // minimum thickness / width
    Screw = [3.0,6.75,25.0]; // holding it all together, OD = washer
    Insert = [3.0,5.5,8.2]; // brass insert
    //Insert = [4.0,6.0,10.0];
    Clamp = [43.2,44.5,34.0]; // tool clamp ring, OD = clearance around top
    LipHeight = IntegerMultiple(2.0,ThreadThick); // above clamp for retaining
    BottomExtension = 25.0; // below clamp to reach workpiece
    MountOAL = LipHeight + Clamp[LENGTH] + BottomExtension; // total mount length
    echo(str("Mount OAL: ",MountOAL));
    Plate = [1.5*PenOD,Clamp[ID] – 0*2*WallThick,WallThick]; // spring reaction plate
    NumScrews = 3;
    ScrewBCD = Bearing[OD] + Insert[OD] + 2*WallThick;
    echo(str("Retainer max OD: ",ScrewBCD – Screw[OD]));
    NumSides = 9*4; // cylinder facets (multiple of 3 for lathe trimming)
    // Basic mount shape
    module CNC3018Base() {
    translate([0,0,MountOAL – LipHeight])
    cylinder(d=Clamp[OD],h=LipHeight,$fn=NumSides);
    translate([0,0,MountOAL – LipHeight – Clamp[LENGTH] – Protrusion])
    cylinder(d=Clamp[ID],h=(Clamp[LENGTH] + 2*Protrusion),$fn=NumSides);
    cylinder(d1=Bearing[OD] + 2*WallThick,d2=Clamp[ID],h=BottomExtension + Protrusion,$fn=NumSides);
    }
    // Mount with holes & c
    module Mount() {
    difference() {
    CNC3018Base();
    translate([0,0,-Protrusion]) // bearing
    PolyCyl(Bearing[OD],2*MountOAL,NumSides);
    for (i=[0:NumScrews – 1]) // clamp screws
    rotate(i*360/NumScrews)
    translate([ScrewBCD/2,0,MountOAL – Clamp[LENGTH]])
    rotate(180/8)
    PolyCyl(Insert[OD],Clamp[LENGTH] + Protrusion,8);
    }
    }
    module SpringPlate() {
    difference() {
    cylinder(d=Plate[OD],h=Plate[LENGTH],$fn=NumSides);
    translate([0,0,-Protrusion])
    PolyCyl(Plate[ID],2*MountOAL,NumSides);
    translate([0,0,Plate.z – SpringSeat[LENGTH]]) // spring retaining recess
    PolyCyl(SpringSeat[OD],SpringSeat[LENGTH] + Protrusion,NumSides);
    for (i=[0:NumScrews – 1]) // clamp screws
    rotate(i*360/NumScrews)
    translate([ScrewBCD/2,0,-Protrusion])
    rotate(180/8)
    PolyCyl(Screw[ID],2*MountOAL,8);
    }
    }
    //—–
    // Build it
    if (Layout == "Base")
    CNC3018Base();
    if (Layout == "Mount")
    Mount();
    if (Layout == "Plate")
    SpringPlate();
    if (Layout == "Show") {
    Mount();
    translate([0,0,1.25*MountOAL])
    rotate([180,0,0])
    SpringPlate();
    }
    if (Layout == "Build") {
    translate([0,-0.75*Clamp[OD],MountOAL])
    rotate([180,0,0])
    Mount();
    translate([0,0.75*Plate[OD],0])
    SpringPlate();
    }