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

  • Chocolate Mold Height Map

    Given that you really don’t care about the absolute dimensions, you can generate a positive mold from a height map image and avoid the entire solid modeling process. Having already solved the cookie press problem, this was a quick-and-easy feasibility study…

    Start by selecting the logo, growing the selection by a few pixels, and feathering the edges to produce the mold draft. Then apply a square gradient behind the Squidwrench logo to produce the height map for the edge of the mold. This one is scaled at 3.0 pixel/mm and is 100×100 pixel, thus producing a 33 mm square mold:

    Squidwrench Mold Pocket

    One could, of course, produce a non-square mold with a different gradient outline shape.

    Hand the image to a slightly modified version of the cookie press script (see below) to get an STL file of the mold:

    SqWr solid model - oblique view
    SqWr solid model – oblique view

    Feed the STL into Slic3r, hand the G-Code to Pronterface, fire the M2!, and you get a positive mold that looks enough like black chocolate to seem ready-to-eat:

    SqWr - mold positive
    SqWr – mold positive

    I have no idea whether that will work as a mold, but I suspect flexy silicone putty won’t reproduce much of the fine plastic filament detail, so the negative mold won’t grab the chocolate. The logo is six threads deep with a little bit of draft, if that makes any difference.

    The backing plate is 1 mm thick and the height map is 5 mm stacked atop that. A few iterations suggested using about 0.75 gray for the logo; working backwards says 5 mm = 25 layers @ 0.20 mm/layer, so a depth of 0.25 * 25 is about six threads.

    For production use, I’d be tempted to import maybe a dozen copies of the STL into OpenSCAD, mount them on a platform with a gutter and a lip on the outside, and then print the whole positive multi-cavity mold in one shot.

    The Bash script that produces the mold strongly resembles my cookie cutter script and contains about as much cruft as you’d expect. Because we need a positive mold, not a negative press, the script doesn’t invert the colors or flop the image left-to-right, nor does it generate the cookie cutter STL around the outside of the press:

    #!/bin/bash
    DotsPerMM=3.0
    MapHeight=7
    ImageName="${1%%.*}"
    rm ${ImageName}_* ${ImageName}-press.stl ${ImageName}-cutter.stl
    echo Normalize and prepare grayscale image...
    convert $1 -type Grayscale -depth 8 -auto-level -trim +repage -flip +set comment ${ImageName}_prep.png
    echo Create PGM files...
    convert ${ImageName}_prep.png -compress none ${ImageName}_map.pgm
    convert ${ImageName}_prep.png -white-threshold 1 -compress none ${ImageName}_plate.pgm
    echo Create height map data files...
    ImageX=`identify -format '%[fx:w]' ${ImageName}_map.pgm`
    ImageY=`identify -format '%[fx:h]' ${ImageName}_map.pgm`
    echo Width: ${ImageX} x Height: ${ImageY}
    cat ${ImageName}_map.pgm   | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_map.dat
    cat ${ImageName}_plate.pgm | tr -s ' \012' '\012' | tail -n +5 | column -x -c $((8*$ImageX)) > ${ImageName}_plate.dat
    echo Create cookie press...
    time openscad -D BuildPress=true \
    -D fnPlate=\"${ImageName}_plate.dat\" \
    -D fnMap=\"${ImageName}_map.dat\" -D Height=$MapHeight \
    -D ImageX=$ImageX -D ImageY=$ImageY -D DotsPerMM=$DotsPerMM \
    -o ${ImageName}-press.stl Cookie\ Cutter.scad
    

    The OpenSCAD program are unchanged from the cookie cutter process.

  • Renaming Files With Sequential Numbers, Plus Moviemaking Therefrom

    The avconv (formerly ffmpeg) image-to-video programs expect sequentially numbered files, with the numbers in a fixed-width part of the file name, thusly: dsc00001.jpg.

    Given a set of files (previously normalized to lowercase) like this:

    ll | head
    total 286576
    -rwxr-xr-x 1 ed ed 595708 Jan 23 19:14 dsc00940.jpg
    -rwxr-xr-x 1 ed ed 515561 Jan 23 19:14 dsc00941.jpg
    -rwxr-xr-x 1 ed ed 580190 Jan 23 19:14 dsc00942.jpg
    -rwxr-xr-x 1 ed ed 571387 Jan 23 19:14 dsc00943.jpg
    -rwxr-xr-x 1 ed ed 573207 Jan 23 19:14 dsc00944.jpg
    -rwxr-xr-x 1 ed ed 571086 Jan 23 19:14 dsc00945.jpg
    -rwxr-xr-x 1 ed ed 571600 Jan 23 19:14 dsc00946.jpg
    -rwxr-xr-x 1 ed ed 571547 Jan 23 19:14 dsc00947.jpg
    -rwxr-xr-x 1 ed ed 565706 Jan 23 19:15 dsc00948.jpg
    

    A Bash one-liner loop does the renumbering:

    sn=1 ; for f in *jpg ; do printf -v dn 'dsc%05d.jpg' "$(( sn++ ))" ; mv $f $dn ; done
    

    The results look pretty much like you’d expect:

    ll | head
    total 286556
    -rwxr-xr-x 1 ed ed 595708 Jan 23 19:14 dsc00001.jpg
    -rwxr-xr-x 1 ed ed 515561 Jan 23 19:14 dsc00002.jpg
    -rwxr-xr-x 1 ed ed 580190 Jan 23 19:14 dsc00003.jpg
    -rwxr-xr-x 1 ed ed 571387 Jan 23 19:14 dsc00004.jpg
    -rwxr-xr-x 1 ed ed 573207 Jan 23 19:14 dsc00005.jpg
    -rwxr-xr-x 1 ed ed 571086 Jan 23 19:14 dsc00006.jpg
    -rwxr-xr-x 1 ed ed 571600 Jan 23 19:14 dsc00007.jpg
    -rwxr-xr-x 1 ed ed 571547 Jan 23 19:14 dsc00008.jpg
    -rwxr-xr-x 1 ed ed 565706 Jan 23 19:15 dsc00009.jpg
    

    Because you’re renaming the files anyway, don’t bother to normalize ’em:

    sn=1 ; for f in *JPG ; do printf -v dn 'dsc%05d.jpg' "$(( sn++ ))" ; mv $f $dn ; done
    

    And, of course, you can fetch ’em from the camera while doing that:

    sn=1 ; for f in /mnt/part/DCIM/100MSDCF/*JPG ; do printf -v dn 'dsc%05d.jpg' "$(( sn++ ))" ; cp -a $f $dn ; done
    

    That leaves the DSC*JPG original files on the camera, where you can delete all of them in one operation when you’re happy with the results.

    If you don’t need the full resolution, reserialize and resize each picture on the fly:

    sn=1 ; for f in /mnt/part/DCIM/100MSDCF/*JPG ; do printf -v dn 'dsc%05d.jpg' "$(( sn++ ))" ; convert $f -resize 50% $dn ; done
    

    That’s based on combining several hints turned up by the usual Google search.

    To assemble a quick-and-simple movie from the images:

    avconv -r 30 -i dsc%05d.jpg -q 5 movie.mp4
    

    The image quality certainly isn’t up to what you (well, I) would expect from a 1920×1080 “HD” file, but the Sony HDR-AS30V Zeiss camera lens seems to be a fisheye pinhole (170° view angle, 2.5 mm f/2.8) backed with relentless image compression:

    Sony HDR-AS30V Action Camera
    Sony HDR-AS30V Action Camera

    Memo to Self: It’s not worth creating and remembering Yet Another Script.

  • Changing the Case of a FAT File Name

    While converting a stop-action series of images from the HDR-AS30V into a movie, I wanted change all the image files on a USB Flash drive from DSC00008.JPG to dsc00008.jpg, so as to simplify typing their names.

    Alas, because the camera’s exFAT filesystem cares not one whit about case, the obvious command doesn’t work:

    rename 's/JPG/jpg/' /mnt/part/*
    /mnt/part/DSC00008.JPG not renamed: /mnt/part/DSC00008.jpg already exists
    

    So you must do each piece in two steps:

    rename 's/JPG/jpgx/' /mnt/part/*
    rename 's/jpgx/jpg/' /mnt/part/*
    rename 's/DSC/dscx/' /mnt/part/*
    rename 's/dscx/dsc/' /mnt/part/*
    

    Obvious once you see it, I suppose…

    See the comments for a better way:

    rename 'y/A-Z/a-z/' *JPG
    
  • Sony NP-BX1 Battery Test Fixture

    The Sony HDR-AS30V “action camera” uses NP-BX1 lithium batteries (3.7 V @ 1.24 A·h = 4.6 W·h) that are, of course, a completely different size and shape than any other lithium battery on the planet.

    So.

    Tweaking a few dimensions in the Canon NB-6L source code, tinkering with the layout of the contact pins, and shazam Yet Another 3D Printed Battery Test Fixture:

    NP-BX1 Holder - show layout
    NP-BX1 Holder – show layout

    It builds nicely, although the contact pin tunnels are a bit too close to the top of the case:

    Sony NP-BX1 Holder - on platform
    Sony NP-BX1 Holder – on platform

    After reaming out the contact pin holes to the proper diameters & depths, then gluing the plugs in place, it works just as you’d expect:

    Sony NP-BX1 battery holder
    Sony NP-BX1 battery holder

    It’s worth noting that the Wasabi charger accepts the batteries upside-down, with the conspicuous chevron against the charger body. It’s definitely not the way all the other chargers work. The keying recesses on the battery (corresponding to the blocks in the solid model) lie along the bottom edge of the contact surface, so flipping the battery over means they’ll hold it in place, but … oh, well.

    That grotty Powerpole connector last saw use in some random benchtop lashup. At some point I’ll be forced to start making more of those.

    The OpenSCAD source code:

    // Holder for Sony NP-BX1 Li-Ion battery
    // Ed Nisley KE4ZNU January 2013
    
    include <MCAD/boxes.scad>
    
    // Layout options
    
    Layout = "Show";					//  Show Build Fit Case Lid Pins Plugs AlignPins
    
    //- Extrusion parameters - must match reality!
    //  Print with +2 shells and 3 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    inch = 25.4;
    
    BuildOffset = 3.0;			// clearance for build layout
    
    Gap = 8.0;					// separation for Fit parts
    
    //- Battery dimensions - rationalized from several samples
    //  Coordinate origin at battery corner by contact plates on bottom surface
    
    BatteryLength = 43.0;
    BatteryWidth = 30.0;
    BatteryThick =  9.5;
    
    ContactWidth = 2.90;
    ContactLength = 4.30;
    ContactRecess = 0.90;
    
    ContactOC = 10.0;			// center-to-center across contact face
    ContactOffset = 6.20;		// offset from battery edge
    ContactHeight = 6.30;		// offset from battery bottom plane
    
    AlignThick = 2.75;			// alignment recesses on contact face
    AlignDepth = 1.70;			// into face
    AlignWidth1 = 3.70;			// across face at contacts
    AlignWidth2 = 3.60;			//  ... other edge
    
    //- Pin dimensions
    
    PinTipDia = 1.6;
    PinTipLength = 10.0;
    
    PinTaperLength = 2.3;
    
    PinShaftDia = 2.4;
    PinShaftLength = 6.8;
    
    PinFerruleDia = 3.1;
    PinFerruleLength = 2.0;
    
    PinLength = PinTipLength + PinTaperLength + PinShaftLength + PinFerruleLength;
    
    ExtendRelax = 1.5 + ContactRecess;		// pin extension when no battery is present
    ExtendOvertravel = 1.0;					//  ... beyond engaged position
    
    //- Spring dimensions
    
    SpringDia = 3.1;						// coil OD
    SpringMax = 9.3;
    SpringLength = SpringMax - 0.5;			// slightly compressed
    SpringMin = 4.5;
    
    SpringPlugOD = IntegerMultiple(5.0,ThreadWidth);		// plug retaining the spring
    SpringPlugID = 2.0;
    SpringPlugLength = IntegerMultiple(4.0,ThreadWidth);
    SpringPlugSides = 3*4;
    
    SpringTravel = ExtendRelax + ExtendOvertravel;
    
    //- Holder dimensions
    
    GuideRadius = ThreadWidth;			// friction fit ridges
    GuideOffset = 7;					// from compartment corners
    WallThick = 4*ThreadWidth;			// holder sidewalls
    
    BaseThick = 6*ThreadThick;			// bottom of holder to bottom of battery
    TopThick = 6*ThreadThick;			// top of battery to top of holder
    
    ThumbRadius = 10.0;			// thumb opening at end of battery
    
    CornerRadius = 3*ThreadThick;			// nice corner rounding
    
    CaseLength = SpringPlugLength + SpringLength + PinLength - ExtendRelax
    			+ BatteryLength + GuideRadius + WallThick;
    CaseWidth = 2*WallThick + 2*GuideRadius + BatteryWidth;
    CaseThick = BaseThick + BatteryThick + TopThick;
    
    AlignPinOD = 1.75;			// lid alignment pins - filament snippets
    AlignPinLength = 5.0;
    AlignPinInset = 7.0;
    AlignPinOffset = -3.75;		//  from centerline - choose to miss contact pins
    
    //- XY origin at front left battery corner, Z on platform below that
    
    CaseLengthOffset = -(SpringPlugLength + SpringLength + PinLength - ExtendRelax);
    CaseWidthOffset = -(WallThick + GuideRadius);
    CaseThickOffset = BaseThick;
    
    LidLength = ExtendRelax - CaseLengthOffset;
    
    echo(str("Contact pin tip dia: ",PinTipDia));
    echo(str("Drill depth to taper end: ",
    		 (SpringPlugLength + SpringLength + PinFerruleLength + PinShaftLength + PinTaperLength),
    		 " -- Dia: ",PinShaftDia));
    echo(str("            to ferrule end: ",
    		  (SpringPlugLength + SpringLength + PinFerruleLength),
    		 " -- Dia: ",PinFerruleDia));
    echo(str("            to plug end: ",SpringPlugLength,
    		 " -- Dia: ",SpringPlugOD));
    
    //----------------------
    // Useful routines
    
    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);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      Range = floor(50 / Space);
    
    	for (x=[-Range:Range])
    	  for (y=[-Range:Range])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    //-------------------
    
    //-- Guides for tighter friction fit
    
    module Guides() {
      	  translate([GuideOffset,-GuideRadius,CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([GuideOffset,(BatteryWidth + GuideRadius),CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([(BatteryLength - GuideOffset),-GuideRadius,CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([(BatteryLength - GuideOffset),(BatteryWidth + GuideRadius),CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([(BatteryLength + GuideRadius),GuideOffset/2,CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    	  translate([(BatteryLength + GuideRadius),(BatteryWidth - GuideOffset/2),CaseThickOffset])
    		PolyCyl(2*GuideRadius,(BatteryThick - Protrusion),4);
    
    }
    
    //-- Contact pins (holes therefore)
    
    module PinShape() {
    
      union() {
    	cylinder(r=(PinTipDia + HoleWindage)/2,h=(PinTipLength + Protrusion),$fn=6);
    
    	translate([0,0,PinTipLength])
    	  cylinder(r=(PinShaftDia + HoleWindage)/2,
    			   h=(PinTaperLength + PinShaftLength + Protrusion),$fn=6);
    
    	translate([0,0,(PinLength - PinFerruleLength)])
    	  cylinder(r=(PinFerruleDia + HoleWindage)/2,
    				h=(PinFerruleLength + Protrusion),$fn=6);
    
    	translate([0,0,(PinLength)])
    	  cylinder(r=(SpringDia + HoleWindage)/2,
    				h=(SpringLength + Protrusion),$fn=6);
    
    	translate([0,0,(PinLength + SpringLength - HoleWindage)])	// windage for hole length
    	  cylinder(r=(SpringPlugOD + HoleWindage)/2,h=3*SpringPlugLength,$fn=SpringPlugSides);
    
    //	  translate([0,0,(PinLength + SpringLength + SpringPlugLength)])
    //	  cylinder(r=(SpringPlugOD + HoleWindage)/2,h=2*SpringPlugLength,$fn=SpringPlugSides);	// extend hole
      }
    
    }
    
    module PinAssembly() {
    
      translate([ExtendRelax,ContactOffset,CaseThickOffset + ContactHeight]) {
    	rotate([0,270,0]) {
    	  PinShape();												// pins
    	  translate([0,(1*ContactOC),0])
    		PinShape();
    	}
      }
    
    }
    
    //-- Alignment pins
    
    module AlignPins() {
    
    	for (x=[-1,1])
    		translate([x*(LidLength - 2*AlignPinInset)/2,AlignPinOffset,0])
    			rotate(45)
    			PolyCyl(AlignPinOD,AlignPinLength);
    }
    
    //-- Case with origin at battery corner
    
    module Case() {
    
      difference() {
    
    	union() {
    
    	  difference() {
    		translate([(CaseLength/2 + CaseLengthOffset),
    				  (CaseWidth/2 + CaseWidthOffset),
    				  (CaseThick/2)])
    		  roundedBox([CaseLength,CaseWidth,CaseThick],CornerRadius); 	// basic case shape
    
    		translate([-ExtendOvertravel,-GuideRadius,CaseThickOffset])
    		  cube([(BatteryLength + GuideRadius + ExtendOvertravel),
    				(BatteryWidth + 2* GuideRadius),
    				(BatteryThick + Protrusion)]);						// battery space
    
    	  }
    
    	  Guides();
    
    	  translate([-ExtendOvertravel,-GuideRadius,BaseThick])
    		cube([(AlignDepth + ExtendOvertravel),
    			  (AlignWidth1 + GuideRadius),
    			  AlignThick]);											// alignment blocks
    	  translate([-ExtendOvertravel,
    				 (BatteryWidth - AlignWidth2),
    				 BaseThick])
    		cube([(AlignDepth + ExtendOvertravel),
    			  (AlignWidth2 + GuideRadius),
    			  AlignThick]);
    
    	}
    
    	translate([(-ExtendOvertravel),
    			   (CaseWidthOffset - Protrusion),
    			   (CaseThickOffset + BatteryThick)])
    	  cube([CaseLength,
    		    (CaseWidth + 2*Protrusion),
    		    (TopThick + Protrusion)]);								// battery access
    
    	translate([(CaseLengthOffset - Protrusion),
    			   (CaseWidthOffset - Protrusion),
    			   (CaseThickOffset + BatteryThick)])
    	  cube([(CaseLength + 2*Protrusion),
    		    (CaseWidth + 2*Protrusion),
    		    (TopThick + Protrusion)]);								// battery insertion allowance
    
    	translate([(BatteryLength - Protrusion),
    			    (CaseWidth/2 + CaseWidthOffset),
    			    (CaseThickOffset + ThumbRadius)])
    	  rotate([90,0,0])
    		rotate([0,90,0])
    		  cylinder(r=ThumbRadius,
    				   h=(WallThick + GuideRadius + 2*Protrusion),
    				   $fn=22);											// remove thumb notch
    
    	PinAssembly();
    
    	translate([-LidLength/2,BatteryWidth/2,CaseThick - TopThick - (AlignPinLength - TopThick/2)])
    		AlignPins();
      }
    
    }
    
    module Lid() {
    
      difference() {
    	translate([0,0,(CaseThick/2 - BaseThick - BatteryThick)])
    	  roundedBox([LidLength,
    				 CaseWidth,CaseThick],CornerRadius);
    
    	translate([0,0,-(CaseThick/2)])
    	  cube([(LidLength + 2*Protrusion),
    		    (CaseWidth + 2*Protrusion),
    		    (CaseThick)],center=true);
    
    	translate([-ExtendRelax,0,-(AlignPinLength - TopThick/2)])
    		AlignPins();
      }
    
    }
    
    module PlugShape() {
    
      difference() {
    	cylinder(r=SpringPlugOD/2,h=SpringPlugLength,$fn=SpringPlugSides);
    	translate([0,0,-Protrusion])
    	  PolyCyl(SpringPlugID,(SpringPlugLength + 2*Protrusion),SpringPlugSides);
      }
    }
    
    module Plugs() {
      translate([0,ContactOC,0])
    	PlugShape();
      translate([0,-ContactOC,0])
    	PlugShape();
    }
    
    //-------------------
    // Build it!
    
    ShowPegGrid();
    
    if (Layout == "Case")
      Case();
    
    if (Layout == "Lid")
      Lid();
    
    if (Layout == "Plugs")
    	for (i=[-1:1])
    		translate([i*1.5*SpringPlugOD,0,0])
    			Plugs();
    
    if (Layout == "Pins")
      PinShape();
    
    if (Layout == "AlignPins")
      AlignPins();
    
    if (Layout == "Show") {								// reveal pin assembly
      difference() {
    	Case();
    
    	translate([(CaseLengthOffset - Protrusion),
    			   (CaseWidthOffset - Protrusion + WallThick + ContactOffset + ContactOC),
    			   (BaseThick + ContactHeight)])
    	  cube([(-CaseLengthOffset + Protrusion),
    			 (CaseWidth + 2*Protrusion),
    			 CaseThick + BaseThick - ContactHeight + Protrusion]);
    
    	translate([(CaseLengthOffset - Protrusion),
    			   (CaseWidthOffset - Protrusion),
    			   -Protrusion])
    	  cube([(-CaseLengthOffset + Protrusion),
    			 (WallThick + GuideRadius + ContactOffset + Protrusion),
    			 CaseThick]);
      }
    
      translate([ExtendRelax,ContactOffset,(CaseThickOffset + ContactHeight)]) {	// pins
    	rotate([0,270,0]) {
    	  %PinShape();
    //	  translate([0,(2*ContactOC),0])
    //		%PinShape();
    	}
      }
    
      translate([CaseLengthOffset,ContactOffset,(CaseThickOffset + ContactHeight)])
    	rotate([0,90,0])
    	  PlugShape();
    }
    
    if (Layout == "Build") {
      translate([-(CaseLength/2 + CaseLengthOffset),-(CaseWidthOffset - BuildOffset),0])
    	Case();
      translate([CaseWidth/2,(CaseLengthOffset/2 - BuildOffset),0])
    	rotate([0,0,90])
    	  Lid();
      for (i=[-1:1])
    	translate([CaseLengthOffset/2 + i*1.5*SpringPlugOD,-CaseWidth/2,0])
    		Plugs();
    }
    
    if (Layout == "Fit") {
      Case();
      translate([(-LidLength/2 + ExtendRelax),
    			(CaseWidth/2 + CaseWidthOffset),
    			(BaseThick + BatteryThick + Gap)])
    	  Lid();
      translate([ExtendRelax,ContactOffset,CaseThickOffset + ContactHeight]) {	// pins
    	rotate([0,270,0]) {
    	  %PinShape();
    	  translate([0,(1*ContactOC),0])
    		%PinShape();
    	}
      }
    
      translate([CaseLengthOffset,
    			(ContactOffset + ContactOC),
    			(CaseThickOffset + ContactHeight)])
      rotate([0,90,0])
    	Plugs();
    
      translate([-LidLength/2,BatteryWidth/2,CaseThick])
    #	AlignPins();
    
    }
    
  • Modified Quilting Foot: Speed Wrench Knob

    The Nyloc nut atop that modified quilting foot requires more grip than fingers can provide:

    Modified Darning Foot - in action
    Modified Darning Foot – in action

    The “precision” wrench I adapted to that nut works for small adjustments, but for larger ones it’s easier to take the foot off and spin this knob:

    Quilting Foot Knob - knurling
    Quilting Foot Knob – knurling

    It has a hex opening in each end that fits the nut, with a through hole for the bolt. The top looks exactly like you’d expect:

    Quilting Foot Knob - top
    Quilting Foot Knob – top

    The bottom needs a bit of support:

    Quilting Foot Knob - bottom support
    Quilting Foot Knob – bottom support

    The solid model shows off the support in color:

    Quilting Foot Knob
    Quilting Foot Knob

    The OpenSCAD source code doesn’t have many surprises:

    // Quilting foot knob
    // Ed Nisley KE4ZNU January 2013
    
    use <knurledFinishLib_v2.scad>
    
    //- Extrusion parameters must match reality!
    //  Print with +1 shells and 3 solid layers
    
    ThreadThick = 0.20;
    ThreadWidth = 0.40;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    //----------------------
    // Dimensions
    
    KnobOD = 20.0;
    KnobLength = 25.0;
    KnobSides = 12;
    
    DiamondLength = KnobLength/3;
    DiamondWidth = DiamondLength/2;
    DiamondDepth = 1.0;
    
    NutOD = 7.0;				// across flats!
    NutLength = 6.0;
    ScrewOD = 4.0;
    
    DoSupport = true;
    
    //----------------------
    // Useful routines
    
    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);
    }
    
    module ShowPegGrid(Space = 10.0,Size = 1.0) {
    
      RangeX = floor(100 / Space);
      RangeY = floor(125 / Space);
    
    	for (x=[-RangeX:RangeX])
    	  for (y=[-RangeY:RangeY])
    		translate([x*Space,y*Space,Size/2])
    		  %cube(Size,center=true);
    
    }
    
    module Knob() {
    	rotate(180/Sides) {
    		difference() {
    //			cylinder(r=KnobOD/2,h=KnobLength,$fn=KnobSides);
    			render(convexity=10)
    			knurl(k_cyl_hg=KnobLength,
    				  k_cyl_od=KnobOD,
    				  knurl_wd=DiamondWidth,
    				  knurl_hg=DiamondLength,
    				  knurl_dp=DiamondDepth,
    				  e_smooth=DiamondLength/2);
    			translate([0,0,-Protrusion])
    				PolyCyl(ScrewOD,(KnobLength + 2*Protrusion),6);
    			translate([0,0,(KnobLength - NutLength)])
    				PolyCyl(NutOD,(NutLength + Protrusion),6);
    			translate([0,0,-Protrusion])
    				PolyCyl(NutOD,(NutLength + Protrusion),6);
    		}
    	}
    }
    
    module Support() {
    	color("Yellow")
    		for (Seg=[0:5]) {
    			rotate(360*Seg/6)
    			translate([0,0,(NutLength - ThreadThick)/2])
    				cube([(NutOD - 1*ThreadWidth),
    						2*ThreadWidth,
    						(NutLength - ThreadThick)],
    						center=true);
    			}
    }
    
    //----------------------
    // Build them!
    
    ShowPegGrid();
    
    Knob();
    
    if (DoSupport)
    	Support();
    

    Mary likes it… and thinks I’m being silly. She’s right, of course.

  • Forcing Pulseaudio DisplayPort Volume Initialization

    It turns out that the audio-over-HDMI/DisplayPort channel which, for whatever reason, is the only way to get audio out of the Optiplex 980 with the big Dell U2711 monitor starts up AT MAXIMUM VOLUME! regardless of the GUI’s Pulseaudio mixer setting that’s diligently saved-and-restored across sessions. That makes a certain perverse sense, as the digital-to-analog converter & power amp live inside the monitor.

    Manually adjusting the GUI mixer by one click, either up or down, forces the new setting out over the digital link to the monitor, after which the audio output corresponds to the mixer; I never remember that until just after some dipshit auto-play video lights up with a fanfare.

    Setting the mixer to the same value doesn’t force an update, so the obvious solution (at least to me) of sending a fixed initial value doesn’t work; it’s optimized away. I think that’s why the initial update doesn’t happen: the stored volume is the same as the, ah, stored volume, so there’s no need to tell the monitor.

    The automatic solution involves putting two more commands in my ever-growing ~/.config/startup.sh:

    amixer -D pulse sset Master 26%
    amixer -D pulse sset Master 1%-
    

    That sets a rational level (which might be the same as the existing one from the previous session), then changing it by one tiny click to force the new value out to the monitor.

    And then It Just Works…

  • Rounded Rectangles in OpenSCAD: Mold Positives?

    A discussion on the OpenSCAD mailing list about making a rectangular solid with rounded edges having different radii eventually produced this delightful result:

    Basic Rounded Cube
    Basic Rounded Cube

    Those guys make me feel dumb, because they’re generally solving problems I can’t even imagine, but I know what to do with this solution. One could slice it in half horizontally, emboss a height map defining a logo / picture into the top surface, print it out on your favorite 3D printer, maybe smooth / seal the surface a bit, define it to be a positive mold pattern, cast / pour flexible silicone around it, and get a negative mold for a pourable precious material such as, oh, chocolate.

    You could make half a dozen of them, arrange them inside a suitable printed frame, pour the silicone, and get a multi-cavity mold for better manufacturing productivity.

    The overall block lacks draft, because the problem it solves presumes you need a block of specific outside dimensions: it overlays three full-size rectangular blocks that define the dimensions. OpenSCAD constructs spheres such that they may be slightly smaller than the defined radius at the poles and, depending on their alignment, a face at the equator may reduce the outer dimension of a surrounding hull.

    Given a sufficiently bendy silicone mold, you might not need any draft at all. If you do need draft and you don’t care about a very slightly undersized pattern, remove the internal blocks and increase the XY spacing of the lower four spheres by enough to make the draft come out right.

    The grayscale logo / image should have nice smooth transitions that produce suitable draft for the fine details; a bare black-and-white image might not work well. Shallow is good, but that conflicts with 3D printing’s crappy resolution: 1 mm = 10 layers, tops. That might not matter in practice.

    You’re supposed to temper the chocolate, but that’s probably more relevant for Fine Art molds.

    The (slightly modified) OpenSCAD source code:

    module rcube(size=[30, 20, 10], radius=[3, 2, 1], center=true)
    	hull() {
    		translate( center ? [0,0,0] : size/2 ) {
    			cube(size-2*radius+[2*radius[0],0,0],center=true);
    			cube(size-2*radius+[0,2*radius[1],0],center=true);
    			cube(size-2*radius+[0,0,2*radius[2]],center=true);
    
    			for(x = [-0.5,0.5], y = [-0.5,0.5], z = [-0.5,0.5])
    				translate([x * ( size[0] - 2*radius[0]),
    						   y * ( size[1] - 2*radius[1]),
    						   z * ( size[2] - 2*radius[2])])
    					scale([radius[0], radius[1], radius[2]])
    						sphere(1.0,$fn=4*4);
    		}
    	}
    
    rcube();
    

    When I get around to doing molds, maybe I can remember what I was thinking…