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

  • Punched Card Production

    Punched Card Production

    For reasons I cannot divulge at the moment, I have undertaken a project requiring Old School punched cards, although they will never be fed through a card reader. Because we live in the future, punched cards are no longer a cheap and readily available resource; I will always deeply regret trashing an entire box back in the day.

    However, living in the future does confer some advantages:

    Punched cards - Apollo 11 CM
    Punched cards – Apollo 11 CM

    The process involves a vast number of moving parts, not all of which I fully understand, but I can (generally) produce consistent results and that must suffice. This post is an overview; I will go into the moving parts in more detail so I can remember why I did what I did.

    A Python program converts a line of text into an SVG file that contains either the card’s printable contents or the paths required to cut its holes & perimeter. A handful of command-line switches determines the outcome, so you run the program twice with different switches for each line of text to get a matched pair of SVG files.

    A Bash script read a text file and hands each line to the Python program, producing two SVG files for each card. It then invokes Inkscape to convert the printable SVG into a PNG image, uses Imagemagic to composite the logo behind the card contents & scale the result to make my printer’s output match the laser’s dead-on positioning, then properly position the card image in a Letter-size PNG image that’s apparently the only way to print it accurately on a punched card:

    Composited Letter layout - exvb-00000710-lt
    Composited Letter layout – exvb-00000710-lt

    That’s not full size.

    N.B.: there’s no such thing as a blank card that will be punched later, because the printed card includes the text across the top. The program also suppresses the row digits where a punch will appear, thus making slight misalignments less painful and mismatched SVG files more obvious.

    Print all the card images on precut 1/3 Letter size sheets of heavy cardstock:

    Ext Verb cards - 0280 skewed print
    Ext Verb cards – 0280 skewed print

    Yes, the printing on the middle card is slightly skewed with respect to the precut card blank. The overall process must handle about two millimeters of positioning inaccuracy and whatever angular skew comes from the printer’s paper feed rollers / guides.

    A DOS Windows BAT file feeds the SVG files with the holes & outline paths to LightBurn, one by one. No lie.

    Put each printed card in a fixture and align its targets, whereupon LightBurn evaporates the holes and cuts the outline:

    Punched cards - laser fixture overview
    Punched cards – laser fixture overview

    In my somewhat biased opinion, the results look good:

    Ext Verb cards - 0270-0290 punched
    Ext Verb cards – 0270-0290 punched

    The Python program also produces cards with test patterns useful for wringing out the process:

    Punched cards - character tests
    Punched cards – character tests

    “Punching” a lace card is no problem and, given an all-blank text line, the result looks like a blank card:

    Punched cards - lace and blank tests
    Punched cards – lace and blank tests

    If you happen to have a card punch, be my guest.

    The source text for the cards comes from the Apollo Guidance Computer in the Apollo 11 Command Module, via an amazing GitHub repository. You can run a virtual AGC in the privacy & comfort of your own home.

    Useful links:

  • Olive Oil Bottle Cap Covers

    Olive Oil Bottle Cap Covers

    We buy olive oil in large bottles, then fill smaller bottles for easier handling. The caps on those bottles were never meant to last as long as we keep them and the thin, deeply drawn aluminum tends to crack after a while.

    So I conjured a cap cover from the vasty digital deep:

    Olive Oil Cap - solid model
    Olive Oil Cap – solid model

    Which looks exactly like you’d expect when printed in black PETG:

    Olive oil bottle cap - details
    Olive oil bottle cap – details

    You can see the raggedy edge of the original cap just inside the cover’s rim. A snippet of double-sided tape holds the cover in place, after de-oiling the cap with alcohol.

    Having gotten one to fit, I made enough for All The Bottles:

    Olive oil bottle cap - installed
    Olive oil bottle cap – installed

    Only two of those see regular service: one in use and another filled when the first is nearly empty. The remaining pair huddle in the back of the shelf against future need.

    The OpenSCAD source code produces those fancy knurls with BOSL2’s textured cyl() :

    // Shower soap dish
    // Ed Nisley - KE4ZNU
    // 2026-01-17
    
    include <BOSL2/std.scad>
    
    /* [Hidden] */
    
    HoleWindage = 0.2;
    Protrusion = 0.1;
    NumSides = 5*3*4;
    
    $fn=NumSides;
    
    ID = 0;
    OD = 1;
    LENGTH = 2;
    
    WallThick = 1.0;
    BaseThick = 2.0;
    
    CapOD = 36.0;
    
    CoverOA = [CapOD,CapOD + 2*WallThick,20.0 + BaseThick];
    
    //----------
    // Build it
    
      render()
        difference() {
          cyl(BaseThick,d=CoverOA[OD],chamfer1=1.0,anchor=BOTTOM) position(TOP)
            cyl(CoverOA[LENGTH] - BaseThick,d=CoverOA[OD],
                texture="trunc_pyramids",tex_size=[2,6], style="convex",
                anchor=BOTTOM);
          up(BaseThick)
            cyl(CoverOA[LENGTH],d=CoverOA[ID],anchor=BOTTOM);
        }
    
    
    
  • Translucent Night Light Light Guide

    Translucent Night Light Light Guide

    Our house came with several single-LED night lights featuring a transparent light guide / reflector:

    Nightlight light guide - original
    Nightlight light guide – original

    The plate had snapped off one of them and, being me, I wondered if I could replace it with something similar.

    Years passed.

    Obviously, this must be made from a transparent substance, which 3D printed things are not, but after some fiddling with parameters I thought the result might be informative.

    The guide plate is a section of a spherical surface, here approximated by a BOSL2 spheroid():

    Nightlight light guide - view side - solid model
    Nightlight light guide – view side – solid model

    The original is 3 mm thick, but 2 mm worked out better for my purposes by reducing the amount of infill:

    Nightlight light guide - wall side - solid model
    Nightlight light guide – wall side – solid model

    The intricate base latches into the lamp’s plastic case:

    Nightlight light guide - base - solid model
    Nightlight light guide – base – solid model

    The result is, at best, translucent, because it’s definitely not transparent:

    Nightlight light guide - translucent vs transparent
    Nightlight light guide – translucent vs transparent

    The zigzag pattern seems to come from the icosohedral approximation to the sphere, because it follows the surface tesselation.

    Getting the base shape right required several iterations, each printed with the model cut off just above the bottom of the guide plate:

    Nightlight light guide - test pieces
    Nightlight light guide – test pieces

    The first two attempts needed attention from a flush cutting pliers before fitting into the case, but they don’t call it rapid prototyping for nothin’.

    The original and replacement plugged into an outlet strip:

    Nightlight light guide - original vs printed on outlet strip
    Nightlight light guide – original vs printed on outlet strip

    While you can see the vague outline of the strip behind the printed light guide, it’s definitely lacking in detail:

    Nightlight light guide - outlet strip detail
    Nightlight light guide – outlet strip detail

    The striations throw more light into the room than the original:

    Nightlight light guide - printed
    Nightlight light guide – printed

    Fiddling with the 3D printing parameters might make it more transparent, but it’s going back into the box it came from after giving me a better idea of which parameters to tweak the next time around.

    The OpenSCAD source code as a GitHub Gist:

    // Nightlight light guide
    // Ed Nisley – KE4ZNU
    // 2026-01-13
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Plate,Base,Pipe]
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.1;
    NumSides = 10*3*4;
    $fn=NumSides;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    function ChordRadius(m,c) = (m^2 + (c^2)/4) / (2*m);
    PlateThick = 2.0;
    PlateOA = [60.0,50.0,PlateThick];
    PlateRound = 5.0;
    PlateTaper = 1.0;
    PlateAngle = atan(-2/60); // original plate angle, far end closer to wall
    PlateM = 2.4;
    PlateRadius = ChordRadius(PlateM,PlateOA.x); // light guide plate
    echo(PlateRadius=PlateRadius);
    WallThick = 2.0;
    MountOA = [23.4,17.0,5.5];
    MountRadius = ChordRadius(4.3,MountOA.x); // base arc in housing
    echo(MountRadius=MountRadius);
    PipeThick = 5.0;
    //———-
    // Define shapes
    // Oddly intricate base fitting into housing
    // Replete with magic numbers
    module Base() {
    difference() {
    union() {
    intersection() {
    cuboid([MountOA.x,MountOA.y,5.5],anchor=BOTTOM);
    back(6.5)
    tube(MountOA.z,or=MountRadius,wall=1.5,anchor=BOTTOM+BACK);
    }
    for (i=[-1,1])
    right(i*18.5/2)
    back(11.5)
    cuboid([1.8,8.0,MountOA.z],anchor=BOTTOM+BACK);
    for (i=[-1,1])
    right(i*22.0/2)
    cuboid([1.4,2.0,MountOA.z],anchor=BOTTOM+FRONT);
    fwd(5.0)
    cuboid([11.0,10.5,MountOA.z],anchor=BOTTOM+FRONT);
    }
    down(Protrusion)
    for (j=[-1,1])
    fwd(j*(1.5 + 10.0)/2)
    cuboid([7.0,10.0,MountOA.z + 2*Protrusion],anchor=BOTTOM);
    up(3.1)
    back(7.5)
    cuboid([MountOA.x,25.0,MountOA.z],anchor=BOTTOM+FRONT);
    }
    }
    // Light guide plate
    module Plate() {
    xrot(PlateAngle)
    zrot(90) yrot(90)
    left(PlateOA.x/2)
    down(PlateM + PlateThick/2)
    intersection() {
    up(PlateRadius)
    difference() {
    spheroid(PlateRadius,style="icosa");
    spheroid(PlateRadius – PlateThick,style="icosa");
    }
    cuboid(PlateOA + [0,0,2*PlateThick],rounding=PlateRound,edges="Z",anchor=BOTTOM);
    }
    }
    // Light pipe between base & plate
    // Magic numbers to fit case opening
    module Pipe() {
    difference() {
    intersection() {
    fwd(3.0/2 – 0.2)
    cuboid([MountOA.x,MountOA.y,PipeThick],rounding=0.5,edges="Z",anchor=BOTTOM+FRONT);
    back(6.5)
    cyl(MountOA.z,r=MountRadius,anchor=BOTTOM+BACK);
    }
    down(Protrusion)
    back((1.5 + 10.0)/2)
    cuboid([7.0,10.0,1.0 + Protrusion],anchor=BOTTOM);
    }
    }
    module Assembly() {
    Base();
    up(MountOA.z)
    Pipe();
    up(MountOA.z + PipeThick)
    Plate();
    }
    //———-
    // Build things
    if (Layout == "Base")
    Base();
    if (Layout == "Plate")
    Plate();
    if (Layout == "Pipe")
    Pipe();
    if (Layout == "Show" || Layout == "Build")
    Assembly();
  • Translucent Soap Dishes

    Translucent Soap Dishes

    A SquidWrench meeting discussion about printing transparent objects prompted me to conjure a soap dish from the vasty digital deep:

    Shower Soap Dish - solid model
    Shower Soap Dish – solid model

    They’re all done in “natural” PETG with sufficient variations in speed, temperature, extrusion multiplier, and fill pattern to stock the shower & tub:

    Translucent soap dishes
    Translucent soap dishes

    The single-thread sidewalls came out reasonably translucent in all the variations, but the baseplate remained stubbornly white-ish, even at 20 mm/s and 250 °C with 100% infill. The seams where the extruder retracts and lifts to the next layer remain conspicuous, with a scarf joint forming the white slab in the left-rear dish.

    Quite a while ago, I’d considered making soap dishes with shattered-glass bottoms, but came to my senses. These have some key advantages:

    • Exactly the right size for narrow shower shelves
    • Light enough to not damage anything when it inevitably falls off
    • Reasonably unbreakable when that happens
    • Easily replaced

    They’re also test pieces for the whole transparency thing, so it’s all good.

    The OpenSCAD source code as a GitHub Gist:

    // Shower soap dish
    // Ed Nisley – KE4ZNU
    // 2026-01-13
    include <BOSL2/std.scad>
    /* [Hidden] */
    HoleWindage = 0.2;
    Protrusion = 0.1;
    NumSides = 3*3*4;
    $fn=NumSides;
    ID = 0;
    OD = 1;
    LENGTH = 2;
    WallThick = 0.6;
    BaseThick = 2.0;
    //DishOA = [80.0,40.0,20.0]; // standing used Dove
    DishOA = [90.0,50.0,30.0]; // standing Dove
    //DishOA = [100.0,65.0,30.0]; // half-bar
    DishTaper = [10.0,10.0];
    CornerRadius = 15.0;
    DrainOA = [10.0,DishOA.y,3.0];
    DrainOC = DishOA.x/3;
    //———-
    // Build it
    difference() {
    union() {
    linear_extrude(BaseThick)
    rect([DishOA.x,DishOA.y] – DishTaper,rounding=CornerRadius);
    rect_tube(DishOA.z,wall=WallThick,
    size1=[DishOA.x,DishOA.y] – DishTaper,size2=[DishOA.x,DishOA.y],
    rounding=CornerRadius,anchor=BOTTOM);
    }
    for (j=[-1,1])
    right(j*DrainOC/2)
    up(BaseThick)
    cuboid(DrainOA,rounding=1.0,anchor=BOTTOM+BACK);
    }
  • Ortur YRC-1: Petite Chuck Jaws

    Ortur YRC-1: Petite Chuck Jaws

    The standard jaws for the Ortur Rotary loom over small-diameter workpieces:

    Ortur Rotary Focus Pad - home offset adjustment
    Ortur Rotary Focus Pad – home offset adjustment

    Some measuring and modeling produced petite 3D printed jaws:

    Ortur Rotary - printed jaws
    Ortur Rotary – printed jaws

    Admittedly, those jaws aren’t doing much of anything, but they’re not nearly as much in the way. You (well, I) can screw them in closer to the center to overlap the chuck jaws or another hole outward for slightly larger cylinders.

    The solid model looks about the same:

    Ortur Rotary Jaws - 2-3 show view
    Ortur Rotary Jaws – 2-3 show view

    They build face-down with a little support under the screw recesses for a clean fit on the chuck:

    Ortur Rotary Jaws - Prusaslicer
    Ortur Rotary Jaws – Prusaslicer

    Teeny jaws might be handy:

    Ortur Rotary Jaws - 2-2 show view
    Ortur Rotary Jaws – 2-2 show view

    Screwing them in one hole outward lets them grip medium cylinders without sticking out from the chuck jaws:

    Ortur Rotary - small printed jaws
    Ortur Rotary – small printed jaws

    The OpenSCAD code lets you pick which screw holes you want, but it does not error-check the perverse choices.

    The OpenSCAD source code as a GitHub Gist:

    // Ortur Rotary Focus Pad
    // Ed Nisley – KE4ZNU
    // 2026-01-04
    include <BOSL2/std.scad>
    Style = "Show"; // [Build,Show,Chuck,ChuckJaw,Jaw]
    InnerScrew = 1; // [0:3]
    OuterScrew = 3; // [2:4]
    /* [Hidden] */
    ID = 0;
    OD = 1;
    LENGTH = 2;
    HoleWindage = 0.2;
    Protrusion = 0.1;
    Gap = 5.0;
    NumSides = 8*3*4;
    $fn=NumSides;
    WallThick = 2.0;
    ScrewHead = [4.0 + HoleWindage,7.0 + HoleWindage,4.0];
    ChuckOD = 66.0;
    ChuckThick = 10.0;
    ChuckBCR = [3.5,7.5,15.0,22.5,30.0]; // M4 tapped in chuck jaws
    ChuckJawOA = [ChuckOD/2,8.0 + HoleWindage,3.5];
    JawBlock = [0,15.0,2*WallThick + ScrewHead[LENGTH]]; // .x will be variable
    JawRound = 1.0; // tip rounding
    //—–
    // Single chuck jaw with holes
    module ChuckJaw(Holes=true) {
    difference() {
    intersection() {
    cuboid(ChuckJawOA,anchor=BOTTOM+LEFT);
    cyl(ChuckJawOA.z,d=ChuckOD,anchor=BOTTOM);
    linear_extrude(h=ChuckJawOA.z)
    hexagon(od=ChuckOD,rounding=2.0,anchor=LEFT);
    }
    if (Holes)
    for (i = [0:len(ChuckBCR)-1])
    right(ChuckBCR[i])
    down(Protrusion)
    cyl(2*ChuckJawOA.z,d=ScrewHead[ID],anchor=BOTTOM);
    }
    }
    // Chuck layout
    module Chuck(Holes=true) {
    cyl(ChuckThick,d=ChuckOD,anchor=TOP) position(TOP)
    for (a = [0:120:360])
    zrot(a)
    ChuckJaw(Holes);
    }
    // Gripping jaw
    module Jaw(Screws=[1,3]) {
    HoleOC = ChuckBCR[Screws[1]] – ChuckBCR[Screws[0]];
    JawOAL = HoleOC + ScrewHead[OD] + 2*WallThick + (JawBlock.y/2)/cos(30);
    difference() {
    left(JawOAL/2)
    intersection() {
    cuboid(JawBlock + [JawOAL,0,0],anchor=BOTTOM+LEFT);
    linear_extrude(h=JawBlock.z)
    hexagon(od=ChuckOD,rounding=JawRound,anchor=LEFT);
    right(JawOAL)
    linear_extrude(h=JawBlock.z)
    hexagon(od=ChuckOD,rounding=JawRound,anchor=RIGHT);
    }
    right(0*JawOAL/2)
    for (i=[-1,1])
    right(i*HoleOC/2) {
    down(Protrusion)
    cyl(JawBlock.z,d=ScrewHead[ID],anchor=BOTTOM);
    up(2*WallThick)
    cyl(JawBlock.z,d=ScrewHead[OD],anchor=BOTTOM);
    }
    down(Protrusion)
    cuboid([JawOAL,ChuckJawOA.y,WallThick + Protrusion],anchor=BOTTOM);
    }
    }
    //—–
    // Build things
    if (Style == "Chuck") {
    Chuck();
    }
    if (Style == "Show") {
    xrot(180)
    yrot(90) {
    color("Gray",0.8)
    Chuck();
    up(ChuckJawOA.z – WallThick)
    for (a = [0:120:360])
    zrot(a)
    right((ChuckBCR[InnerScrew] + ChuckBCR[OuterScrew])/2)
    Jaw(Screws=[InnerScrew,OuterScrew]);
    }
    }
    if (Style == "ChuckJaw")
    ChuckJaw();
    if (Style == "Jaw") {
    Jaw(Screws=[InnerScrew,OuterScrew]);
    }
    if (Style == "Build")
    for (j=[-1:1])
    fwd(j*(JawBlock.y + Gap))
    up(JawBlock.z) xrot(180)
    Jaw(Screws=[InnerScrew,OuterScrew]);
  • Mini-lathe Change Gear Generator: Redux

    Mini-lathe Change Gear Generator: Redux

    Because the BOSL2 library includes a gear generator, I can now avoid creating a gear outline in Inkscape and importing it into my stacked change gear generator.

    The labels now snuggle closer to the shaft and (barely) fit on smaller gears:

    Mini-lathe stacked change gears - 28T - solid model
    Mini-lathe stacked change gears – 28T – solid model

    The stacked B-C gears for the jack shaft work as before, with both labels on the top gear:

    Mini-lathe stacked change gears - 28-50T - solid model
    Mini-lathe stacked change gears – 28-50T – solid model

    The admittedly flimsy motivation for all this was to make a 28 tooth gear to cut a 0.9 mm pitch, thus filling an obvious hole in the gear table.

    My collection of gears could do 21-60-81-50, but the 81 T gear collides with the screw holding the 21 T gear. Rearranging it to 21-50-81-60 showed the B-C gears exceeded the space available.

    Because it’s all ratios and a 28 T gear is 4/3 bigger than 21 T, reducing the rest of the train by 3/4 should work. In fact, it produced a reasonable 28-80-81-50 chain:

    Mini-lathe change gears - 28T installed
    Mini-lathe change gears – 28T installed

    The fact that I do not anticipate ever needing to cut a 0.9 mm pitch has nothing whatsoever to do with it; that gear will surely come in handy for something.

    While I was at it, I made a 27 T gear, because 27 = 21 × 9/7:

    Mini-lathe stacked change gears - 27T - PrusaSlicer preview
    Mini-lathe stacked change gears – 27T – PrusaSlicer preview

    You can never have enough change gears. Right?

    The OpenSCAD source code as a GitHub Gist:

    // LMS Mini-Lathe
    // Change gears with stacking
    // Ed Nisley – KE4ZNU
    // 2020-05 use Inkscape SVG gears
    // 2025-12 use BOSL2 gear generator
    include <BOSL2/std.scad>
    include <BOSL2/gears.scad>
    /* [Gears] */
    TopGear = 0; // zero for single gear
    BottomGear = 28;
    /* [Hidden] */
    ThreadThick = 0.20;
    HoleWindage = 0.2;
    Protrusion = 0.1; // make holes end cleanly
    /* [Dimensions] */
    ShaftOD = 12.0;
    GearThick = 7.75;
    Keyway = [3.5,3.0,3*GearThick]; // x on radius, y on perim
    LegendEnable = (TopGear == 0 && BottomGear > 27) || (TopGear > 27);
    LegendThick = 2*ThreadThick;
    LegendZ = (TopGear ? 2*GearThick : GearThick) – LegendThick;
    LegendSize = 5;
    LegendRecess = [8,6,LegendThick];
    LegendOffset = [0,LegendRecess.y/2 + ShaftOD/2 + HoleWindage,LegendZ + LegendRecess.z/2];
    //———————–
    // Build it!
    union() {
    difference() {
    union() {
    spur_gear(mod=1,teeth=BottomGear,thickness=GearThick,shaft_diam=ShaftOD + HoleWindage,anchor=BOTTOM);
    if (TopGear)
    spur_gear(mod=1,teeth=TopGear,thickness=2*GearThick,shaft_diam=ShaftOD + HoleWindage,anchor=BOTTOM);
    }
    right(ShaftOD/2)
    down(Protrusion)
    cube(Keyway,anchor=CENTER+BOTTOM);
    if (LegendEnable) {
    translate(LegendOffset)
    cube(LegendRecess + [0,0,Protrusion],anchor=CENTER);
    if (TopGear)
    zrot(180)
    translate(LegendOffset)
    cube(LegendRecess + [0,0,Protrusion],anchor=CENTER);
    }
    }
    if (LegendEnable)
    translate([0,0,LegendZ – Protrusion])
    linear_extrude(height=LegendThick + Protrusion,convexity=10) {
    translate([LegendOffset.x,LegendOffset.y])
    text(text=str(BottomGear),size=LegendSize,font="Arial:style:Bold",halign="center",valign="center");
    if (TopGear)
    zrot(180)
    translate([LegendOffset.x,LegendOffset.y])
    text(text=str(TopGear),size=LegendSize,font="Arial:style:Bold",halign="center",valign="center");
    }
    }
  • Sears Humidifier Bottle Cap Reinforcement

    Sears Humidifier Bottle Cap Reinforcement

    In the midst of the humidification season, I spotted this while refilling one of the ancient Sears Humidifier bottles:

    Humidifier bottle cap reinforcement - crack
    Humidifier bottle cap reinforcement – crack

    While it’s possible to buy replacement caps, this seemed more appropriate:

    Humidifier bottle cap reinforcement - installed
    Humidifier bottle cap reinforcement – installed

    It’s PETG-CF, of course:

    Bottle cap reinforcement - solid model
    Bottle cap reinforcement – solid model

    The shape is a ring with a simplified model of the cap removed from the middle:

    Bottle cap reinforcement - lid solid model
    Bottle cap reinforcement – lid solid model

    It fits snugly over the cap atop a thin layer of JB PlasticBonder that should hold it in place forevermore:

    Humidifier bottle cap reinforcement - bottom view
    Humidifier bottle cap reinforcement – bottom view

    The other side shows the crack over on the right:

    Humidifier bottle cap reinforcement - top view
    Humidifier bottle cap reinforcement – top view

    Close inspection showed a few smaller cracks, so that cap was likely an original.

    I made another ring for the other cap, only to find it was slightly larger with a black washer inside: apparently a previous owner had replaced one of the caps. The OpenSCAD program has measurements for both, not that you have either.

    The OpenSCAD source code as a GitHub Gist:

    // Humidifier bottle cap reinforcement
    // Ed Nisley – KE4ZNU
    // 2025-11-29
    include <BOSL2/std.scad>
    Layout = "Show"; // [Show,Build,Cap]
    /* [Hidden] */
    Protrusion = 0.1;
    //—–
    // Bottle cap/valve
    // Collects all the magic numbers in one place
    Left = false; // the caps are different, of course
    CapODs = Left ? [43.0,42.1] : [43.1,42.9]; // [0] = base of cap
    CapHeight = 10.0;
    Notch = [0.6,2.0,8.5 + Protrusion]; // Z + hack for slight angle
    NumRibs = 24;
    RibAngle = 90 – atan(CapHeight/((CapODs[0]-CapODs[1])/2));
    echo(RibAngle=RibAngle);
    $fn=2*NumRibs;
    module Cap() {
    difference() {
    cyl(CapHeight,d1=CapODs[1],d2=CapODs[0],anchor=BOTTOM);
    for (a=[0:NumRibs-1])
    zrot(a*360/NumRibs)
    right(CapODs[1]/2) down(Protrusion)
    yrot(RibAngle)
    cuboid(Notch,anchor=RIGHT+BOTTOM);
    }
    }
    //—–
    // Reinforcing ring
    RingThick = 3.0;
    module Ring() {
    render()
    difference() {
    tube(CapHeight,od=CapODs[0] + 2*RingThick,id=CapODs[1] – 2*Notch.x,anchor=BOTTOM);
    Cap();
    }
    }
    // Build things
    if (Layout == "Cap")
    Cap();
    if (Layout == "Build" || Layout == "Show")
    Ring();