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.

Tag: CNC

Making parts with mathematics

  • KG-UV3D GPS+Voice: Box Model

    The first pass at the box that will eventually hold the GPS+voice interface for the KG-UV3D radio looks like this, from the end that engages the alignment tabs on the bottom of the radio:

    Case Solid Model - Tab End View - Fit
    Case Solid Model – Tab End View – Fit

    The other end has the opening for the TT3’s serial connector to the GPS receiver, a probably too-small hole for the external battery pack cable / helmet cable / PTT cable, and a hole on the side for the radio mic/speaker cables.

    Case Solid Model - Connector End View - Fit
    Case Solid Model – Connector End View – Fit

    The serial connector opening has a built-in support plate that’s the shape shrunken by 5% so it’s easy to punch out. That worked surprisingly well; the line just above the right edge isn’t a break, it’s a stack of Reversal Zits. This version is rectangular; the solid model shows the proper D shape.

    KG-UV3D box - connector hole support removal
    KG-UV3D box – connector hole support removal

    The bottom has battery contact recesses and counterbores (if that’s the right term for a molded feature) for the PCB mounting  screws. In retrospect, those holes should be tapping diameter and the screws inserted from the top, through the PCB.

    Case Solid Model - Battery Contact View - Fit
    Case Solid Model – Battery Contact View – Fit

    The colors mark individual pieces that get glued together. I can probably reduce the wall thickness on the top & bottom by three threads, which is in the nature of fine tuning. The latch mechanism that holds this affair to the radio is conspicuous by its absence…

    The OpenSCAD source code:

    // Wouxun KB-UV3D Battery Pack Case
    // Ed Nisley KE4ZNU September 2011
    
    include </home/ed/Thing-O-Matic/lib/MCAD/units.scad>
    include </home/ed/Thing-O-Matic/Useful Sizes.scad>
    include </home/ed/Thing-O-Matic/lib/visibone_colors.scad>
    
    // Layout options
    
    Layout = "Fit";		// Envelope Plate Base Lid Shell Fit Buildx ScrewSupport
    								// PlugPlate
    
    //- Extrusion parameters must match reality!
    //  Print with +1 shells and 3 solid layers
    //  Use 210 C extrusion temperature to improve layer bonding
    
    ThreadThick = 0.33;
    ThreadWidth = 2.0 * ThreadThick;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    BuildOffset = 2.0;			// clearance for build layout
    
    //----------------------
    //- Case dimensions
    
    CaseOverallHeight = 40;
    CaseOverallWidth = 56;
    CaseOverallLength = 80.0;
    
    PlateWidthMin = 53.0;			// plate interfacing with radio contacts
    PlateWidthMax = 54.5;
    PlateLength = 75.0;
    PlateThick = IntegerMultiple(2.0,ThreadThick);
    
    ContactWidth = 7.0 + HoleWindage;
    ContactLength = 7.0 + HoleWindage;
    ContactRecess = 2*ThreadThick;	// recess for contact metal plate
    ContactGapX = 10.5;				// X space between contacts
    Contact1Y = 53.0;				// offset from base
    Contact2Y = 56.5;
    
    BaseWidthInner = PlateWidthMin;
    BaseWidthOuter = CaseOverallWidth;
    BaseLength = CaseOverallHeight;
    BaseThick = 1.0;
    BaseWidthTaper = 5.0;
    
    BaseOpeningMax = 42.0;
    BaseOpeningMin = 33.0;
    BaseOpeningY = 5.25;
    BaseOpeningDepth = 2.0;
    
    BaseTabWidth = 6.0;
    BaseTabThick = 2.0;
    BaseTabGap = 7.0;
    BaseTabOC = BaseTabWidth + BaseTabGap;
    
    BaseToothBase = 6.0;
    BaseToothTip = 3.0;
    BaseToothThick = 2.0;
    BaseToothOC = BaseTabOC;
    
    WedgeAngle = atan(BaseWidthTaper/((BaseWidthOuter - BaseWidthInner)/2));
    echo(str("Plate & Shell Wedge Angle: ",WedgeAngle));
    
    BaseEndLip = ThreadThick;			// should be 0.25 mm or so
    BaseEndWidth = (PlateWidthMin - 3*BaseToothBase - 2*BaseToothTip)/2;
    BaseEndAngle = atan((BaseOpeningDepth - BaseEndLip)/BaseOpeningY);
    
    echo(str("Plate End Angle: ",BaseEndAngle));
    
    PCBWidth = 2.00 * inch;
    PCBLength = 2.75 * inch;
    PCBMargin = Head2_56;
    PCBClearBottom = IntegerMultiple(2*Nut2_56Thick,ThreadThick);
    PCBHoleDia = Tap2_56;
    PCBHoleY = 2.50 * inch;
    PCBHoleX = 1.75 * inch;
    
    echo(str("PCB Mounting Holes OC X: ",PCBHoleX," Y: ",PCBHoleY));
    echo(str("       bottom clearance: ",PCBClearBottom));
    
    ShellHeight = CaseOverallHeight - PlateThick;
    ShellWidth = CaseOverallWidth;
    ShellLength = PlateLength;
    ShellWallX = IntegerMultiple((ShellWidth - PCBWidth)/2,ThreadWidth);
    ShellWallY = IntegerMultiple((ShellLength - PCBLength)/2,ThreadWidth);
    ShellWallMax = max(ShellWallX,ShellWallY);
    
    echo(str("Wall thick X: ",ShellWallX," Y: ",ShellWallY));
    
    LidThick = IntegerMultiple(1.0,ThreadThick);
    LidMargin = IntegerMultiple(1.0,ThreadWidth);
    LidWidth = ShellWidth - 2*LidMargin;
    LidLength = ShellLength - 2*LidMargin;
    
    LidScrewHead = Head3_48;
    LidScrewTap = Tap3_48;
    LidScrewClear = Clear3_48;
    LidScrewLength = 5.0;
    LidScrewOffsetX = ShellWidth/2 - LidMargin - 0.75*LidScrewHead;
    LidScrewOffsetY = ShellLength/2 - LidMargin - 0.75*LidScrewHead;
    
    HTCableDia = 5.0;
    HTCableAspect = 2.0;			// width of hole
    HTCableY = 65;
    HTCableZ = 10;
    
    SerialZ = ShellHeight - LidThick - 12.0;
    
    BikeCableDia = 5.0;
    BikeCableAspect = 1.5;
    BikeCableX = -20;
    BikeCableZ = 15;
    
    JackOC = 11.20;						// 14.25 OD - (3.58 + 2.58)/2
    
    JackScrewDia = 4.6;
    JackScrewOffsetX = 1.00;
    JackScrewOffsetY = 5.25;				//  mounting screw to edge of lower recess
    
    PlugBaseWidth = 9.25;				// lower section of plate
    PlugBaseLength = 22.0;
    PlugBaseThick = 2.5;
    PlugBaseRadius = 1.75;
    
    Plug3Offset = 5.25;					// edge of base recess to 3.5 mm jack
    
    Plug2BezelDia = 7.1;				// 2.5 mm plug
    Plug2BezelThick = 1.04;
    Plug2ScrewDia = 6.0;
    Plug3ScrewLength = 3.0;
    
    Plug3BezelDia = 8.13;				// 3.5 mm plug
    Plug3BezelThick = 1.6;
    Plug3ScrewDia = 7.95;
    Plug3ScrewLength = 4.0;
    
    PlugFillOffsetX = JackScrewOffsetX - 0.5;		// base recess CL to fill CL
    PlugFillOffsetY = -10.5;				//  ... to edge of fill plate
    PlugFillWidth = 11.0;
    PlugFillLength = 34.00;
    PlugFillThick = 3.0;
    PlugFillRadius1 = 1.5;
    PlugFillRadius2 = 4.5;
    
    PlugFillOffsetYTotal = 0;
    
    //----------------------
    // 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);
    
    }
    
    //- Hack job for DB-9 (DE-9) panel opening
    //  Snug fit around the shell surrounding the male pins
    //  DB-9 should mount on outside, but if it's already soldered to the board,
    //   that doesn't work and you must mount it inside the box
    
    module DSubMin9(Height=1.0) {
    
      union() {
    
    	linear_extrude(height=Height,center=false) {
    	  hull() {
    /*
    		translate([-(19.28+0.13)/2,(10.72+0.13)/2,0])			// rectangular outline
    		  circle(r=3.05/2,$fn=8);
    		translate([-(19.28+0.13)/2,-(10.72+0.13)/2,0])
    		  circle(r=3.05/2,$fn=8);
    		translate([ (19.28+0.13)/2,(10.72+0.13)/2,0])
    		  circle(r=3.05/2,$fn=8);
    		translate([ (19.28+0.13)/2,-(10.72+0.13)/2,0])
    		  circle(r=3.05/2,$fn=8);
    */
    		translate([-(17.0+0.05)/2,-(8.48+0.05)/2,0])
    		  circle(r=0.105*inch,$fn=8);
    		translate([ (17.0+0.05)/2,-(8.48+0.05)/2,0])
    		  circle(r=0.105*inch,$fn=8);
    		translate([ (15.5+0.05)/2, (8.48+0.05)/2,0])
    		  circle(r=0.105*inch,$fn=8);
    		translate([-(15.5+0.05)/2, (8.48+0.05)/2,0])
    		  circle(r=0.105*inch,$fn=8);
    	  }
    	  hull() {
    		translate([-24.99/2,0,0])
    		  circle(r=3.05/2,$fn=8);
    		translate([ 24.99/2,0,0])
    		  circle(r=3.05/2,$fn=8);
    	  }
    	}
      }
    
    }
    
    //-------------------
    
    //- Overall case outline
    //  This defines the mating taper into the radio shell
    
    module CaseEnvelope(Length=1) {
    
    	rotate([90,0,0])
    	  linear_extrude(height=Length,center=true,convexity=5)
    		polygon(points=[
    				  [-BaseWidthOuter/2,BaseLength],
    				  [-BaseWidthOuter/2,BaseWidthTaper],
    				  [-BaseWidthInner/2,0],
    				  [-BaseOpeningMax/2,0],
    
    				  [ BaseOpeningMax/2,0],
    				  [ BaseWidthInner/2,0],
    
    				  [ BaseWidthOuter/2,BaseWidthTaper],
    
    				  [ BaseWidthOuter/2,BaseLength]
    				],
    				convexity=1
    		);
    
    }
    
    //- Battery contact plate recess
    //  This gets subtracted from the bottom plate in two places
    
    module Contact() {
    
      union() {
    	translate([0,0,-(ContactRecess - Protrusion)/2])
    	  cube([ContactWidth,ContactLength,(ContactRecess + Protrusion)],center=true);
    	translate([0,0,-(PlateThick + Protrusion)])
    	PolyCyl(Clear3_48,(PlateThick + 2*Protrusion));
    	translate([0,0,-(ContactRecess + Head3_48Thick/3)])
    	  PolyCyl(Head3_48,Head3_48Thick);				// allow for solder blob
      }
    }
    
    //- Back interface plate with battery contacts
    
    module Plate() {
    
      difference() {
    
    	translate([0,PlateLength/2,0])
    	  intersection() {
    		translate([0,0,PlateThick])
    		  rotate([180,0,0])
    			CaseEnvelope(PlateLength);
    	  translate([-PlateWidthMax/2,-PlateLength/2,0])
    		cube([PlateWidthMax,PlateLength,PlateThick],center=false);
    	  }
    
    	translate([-(ContactGapX/2 + ContactWidth/2),(Contact1Y + ContactLength/2),PlateThick])
    	  Contact();
    	translate([+(ContactGapX/2 + ContactWidth/2),(Contact2Y + ContactLength/2),PlateThick])
    	  Contact();
    
    	translate([0,PlateLength/2,0])
    	  PCBHoles(PCBHoleDia,PlateThick);
    
    	translate([0,PlateLength/2,(PlateThick - 2*Head2_56Thick/3)])
    	  PCBHoles(IntegerMultiple(Head2_56,ThreadWidth),IntegerMultiple(Head2_56Thick,ThreadThick));
      }
    
    }
    
    //- Radio bottom locating feature
    //  This polygon gets subtracted from the battery pack base
    
    module RadioBase() {
    
    linear_extrude(height=(BaseOpeningDepth + Protrusion),center=false,convexity=5)
      polygon(points=[
    			[-BaseOpeningMax/2,-Protrusion],
    
    			[-BaseOpeningMin/2,BaseOpeningY],
    			[-(BaseToothOC/2 + BaseToothBase/2),BaseOpeningY],
    
    			[-(BaseToothOC/2 + BaseToothTip/2),(BaseOpeningY - BaseToothThick)],
    			[-(BaseToothOC/2 - BaseToothTip/2),(BaseOpeningY - BaseToothThick)],
    			[-(BaseToothOC/2 - BaseToothBase/2),BaseOpeningY],
    
    			[ (BaseToothOC/2 - BaseToothBase/2),BaseOpeningY],
    			[ (BaseToothOC/2 - BaseToothTip/2),(BaseOpeningY - BaseToothThick)],
    			[ (BaseToothOC/2 + BaseToothTip/2),(BaseOpeningY - BaseToothThick)],
    			[ (BaseToothOC/2 + BaseToothBase/2),BaseOpeningY],
    			[ BaseOpeningMin/2,BaseOpeningY],
    
    			[ BaseOpeningMax/2,-Protrusion],
    
    			[ (BaseTabOC + BaseTabWidth/2),-Protrusion],
    			[ (BaseTabOC + BaseTabWidth/2),BaseTabThick],
    			[ (BaseTabOC - BaseTabWidth/2),BaseTabThick],
    			[ (BaseTabOC - BaseTabWidth/2),-Protrusion],
    
    			[ BaseTabWidth/2,-Protrusion],
    			[ BaseTabWidth/2,BaseTabThick],
    			[-BaseTabWidth/2,BaseTabThick],
    			[-BaseTabWidth/2,-Protrusion],
    
    			[-(BaseTabOC + BaseTabWidth/2),-Protrusion],
    			[-(BaseTabOC + BaseTabWidth/2),BaseTabThick],
    			[-(BaseTabOC - BaseTabWidth/2),BaseTabThick],
    			[-(BaseTabOC - BaseTabWidth/2),-Protrusion],
    		  ],
    		  convexity=5
      );
    }
    
    //- PCB Mounting Holes
    
    module PCBHoles(HoleDia=PCBHoleDia,Height=1.0) {
    
      for (x=[-1,1])
    	for (y=[-1,1])
    	  translate([(x*PCBHoleX/2),
    				(y*PCBHoleY/2),
    				-Protrusion])
    		PolyCyl(HoleDia,(Height + 2*Protrusion));
    
    }
    
    //-- Battery pack base
    
    module Base() {
    
      difference() {
    
    	translate([0,0,(BaseThick + BaseOpeningDepth)/2])
    	  rotate([-90,0,0])
    		CaseEnvelope(BaseThick + BaseOpeningDepth);
    
    	translate([0,0,BaseThick])
    	  RadioBase();
    
    	translate([(BaseToothOC + BaseTabWidth/2),
    			  -(BaseThick + BaseEndLip)/tan(BaseEndAngle),
    			  0])
    	  rotate([BaseEndAngle,0,0])
    		cube([BaseEndWidth,3*BaseOpeningY,BaseOpeningDepth],center=false);
    
    	translate([-(BaseToothOC + BaseTabWidth/2 + BaseEndWidth),
    			  -(BaseThick + BaseEndLip)/tan(BaseEndAngle),
    			  0])
    	  rotate([BaseEndAngle,0,0])
    		cube([BaseEndWidth,3*BaseOpeningY,BaseOpeningDepth],center=false);
      }
    }
    
    //- Lid
    
    module Lid(WithHoles = false) {
    
      translate([0,LidLength/2,LidThick/2])
    	difference() {
    	  cube([LidWidth,LidLength,LidThick],center=true);
    	  if (WithHoles) {
    		translate([LidScrewOffsetX,LidScrewOffsetY,-(LidThick/2 + Protrusion)])
    		  PolyCyl(LidScrewClear,(LidThick + 2*Protrusion));
    		translate([-LidScrewOffsetX,-LidScrewOffsetY,-(LidThick/2 + Protrusion)])
    		  PolyCyl(LidScrewClear,(LidThick + 2*Protrusion));
    	  }
    	}
    }
    
    //- Lid screw support shape
    
    module LidScrewSupport(WithHole = false) {
    
      SupportSize = IntegerMultiple(LidScrewHead,ThreadWidth);
    
      difference() {
    	translate([0,0,LidScrewLength/2])
    	cube([SupportSize,SupportSize,LidScrewLength],center=true);
    	if (WithHole)
    	  translate([0,0,-Protrusion])
    		PolyCyl(LidScrewTap,(LidScrewLength + 2*Protrusion));
      }
    
      translate([-SupportSize/2,SupportSize/2,-2*SupportSize])
    	rotate([90,0,0])
    	  linear_extrude(height=SupportSize,center=false)
    		polygon(points=[
    				  [0,0],[0,2*SupportSize],[SupportSize,2*SupportSize]]);
    
    }
    
    //- Battery pack shell
    
    module Shell() {
    
      union() {
    	difference() {
    
    	  translate([0,0,-PlateThick])
    		intersection() {
    		  CaseEnvelope(ShellLength);
    		  translate([0,0,(ShellHeight/2 + PlateThick)])
    			cube([ShellWidth,ShellLength,ShellHeight],center=true);
    		}
    
    	  translate([0,-LidLength/2,(ShellHeight - LidThick)])
    		scale([1,1,2])				// ensure clean cut across top
    		  Lid(false);
    
    	  translate([0,0,
    				((ShellHeight - PCBClearBottom - LidThick + Protrusion)/2 + PCBClearBottom)])
    		cube([PCBWidth,PCBLength,
    			 (ShellHeight - PCBClearBottom - LidThick + Protrusion)],
    			 center=true);
    
    	  render()
    		difference() {
    		  translate([0,0,ShellHeight/2])
    			cube([(PCBWidth - 2*PCBMargin),
    				(PCBLength - 2*PCBMargin),
    				(ShellHeight + 2*Protrusion)],
    				center=true);
    		  for (x=[-1,1])
    			for (y=[-1,1])
    			  translate([(x*PCBHoleX/2),(y*PCBHoleY/2),-Protrusion])
    				cylinder(r=PCBMargin,(ShellHeight + 2*Protrusion),$fn=4);
    		}
    
    	  PCBHoles(PCBMargin);
    
    	  translate([-(PCBWidth/2 - Protrusion),(HTCableY - PlateLength/2),HTCableZ])
    		rotate([0,-90,0])
    		  scale([1/HTCableAspect,1,1])
    			PolyCyl(HTCableDia,(ShellWallMax + 2*Protrusion),8);
    
    	  translate([BikeCableX,(PCBLength/2 - Protrusion),BikeCableZ])
    		rotate([0,90,90])
    		  scale([1/BikeCableAspect,1,1])
    			PolyCyl(BikeCableDia,(ShellWallMax + 2*Protrusion),8);
    
    	  translate([0,(PCBLength/2 - Protrusion),SerialZ])
    		rotate([-90,0,0])
    		  DSubMin9(ShellWallMax + 2*Protrusion);
    
    	}
    
      	translate([0,(PCBLength/2 + ThreadWidth/2),SerialZ])
    	  rotate([-90,0,0])
    		scale([0.95,0.95,1])
    		  DSubMin9(ShellWallY - ThreadWidth);		// thin support plug in hole
    
      }
    
    }
    
    //- Speaker-Mic plug mounting plate
    
    module PlugPlate() {
    
      BaseX = PlugBaseWidth/2 - PlugBaseRadius;
      BaseY = PlugBaseLength/2 - PlugBaseRadius;
    
      difference() {
    	union() {
    	  linear_extrude(height=PlugBaseThick,center=false,convexity=3)
    		hull() {
    		  translate([-BaseX,-BaseY,0])
    			circle(r=PlugBaseRadius,$fn=8);
    		  translate([-BaseX, BaseY,0])
    			circle(r=PlugBaseRadius,$fn=8);
    		  translate([ BaseX, BaseY,0])
    			circle(r=PlugBaseRadius,$fn=8);
    		  translate([ BaseX,-BaseY,0])
    			circle(r=PlugBaseRadius,$fn=8);
    		}
    
    	  translate([PlugFillOffsetX,
    				(PlugFillLength/2 - PlugBaseLength/2 + PlugFillOffsetY),
    				PlugBaseThick])
    		linear_extrude(height=PlugFillThick,center=false,convexity=5)
    		  hull() {
    			translate([0,-(PlugFillLength/2 - PlugFillRadius2),0])
    			  circle(r=PlugFillRadius2,$fn=10);
    			translate([-(PlugFillWidth/2 - PlugFillRadius1),-PlugBaseLength/2,0])
    			  circle(r=PlugFillRadius1,$fn=8);
    			translate([-(PlugFillWidth/2 - PlugFillRadius1),
    					  (PlugFillLength/2 - PlugFillRadius1),0])
    			  circle(r=PlugFillRadius1,$fn=8);
    			translate([(PlugFillWidth/2 - PlugFillRadius1),
    					  (PlugFillLength/2 - PlugFillRadius1),0])
    			  circle(r=PlugFillRadius1,$fn=8);
    			translate([(PlugFillWidth/2 - PlugFillRadius1),-PlugBaseLength/2,0])
    			  circle(r=PlugFillRadius1,$fn=8);
    		  }
    	}
    
    	translate([0,-JackOC/2,-Protrusion])
    	  rotate(360/16) {
    		PolyCyl(Plug3BezelDia,(Plug3BezelThick + Protrusion),8);
    		PolyCyl(Plug3ScrewDia,(PlugBaseThick + PlugFillThick + 2*Protrusion),8);
    	  }
    
    	translate([0,+JackOC/2,-Protrusion])
    	  rotate(360/16) {
    		PolyCyl(Plug2BezelDia,(Plug2BezelThick + Protrusion),8);
    		PolyCyl(Plug2ScrewDia,(PlugBaseThick + PlugFillThick + 2*Protrusion),8);
    	  }
    
    	translate([JackScrewOffsetX,-(PlugBaseLength/2 + JackScrewOffsetY),0])
    	  PolyCyl(JackScrewDia,(PlugBaseThick + PlugFillThick + Protrusion));
      }
    
    }
    
    //-------------------
    // Build it!
    
    ShowPegGrid();
    
    if (Layout == "Envelope")
      CaseEnvelope(CaseOverallLength);
    
    if (Layout == "Plate")
      Plate();
    
    if (Layout == "Base")
      Base();
    
    if (Layout == "Lid")
      Lid(true);
    
    if (Layout == "ScrewSupport")
      LidScrewSupport(true);
    
    if (Layout == "Shell")
      Shell();
    
    if (Layout == "PlugPlate")
      PlugPlate();
    
    if (Layout == "DSub")
      DSubMin9();
    
    if (Layout == "Fit") {
    
      translate([0,-PlateLength/2,0]) {
    
    	translate([0,0,PlateThick])
    	  rotate([0,180,0])
    		color(LOR) Plate();
    
    	rotate([90,0,0])
    	  color(DYO) Base();
    
    	translate([0,LidMargin,10 + (CaseOverallHeight - LidThick)])
    	  color(MOR) Lid(true);
    
    	translate([0,PlateLength/2,PlateThick])
    	    color(MFG) render() Shell();
    
    	translate([-(ShellWidth/2 +10),70,15])
    	  rotate([0,-90,0])
    		color(DDY) PlugPlate();
      }
    }
    
    if (Layout == "Build1") {
    
      translate([-20,-PlateLength/2,0])
    	Plate();
    
        translate([10,0,0])
    	rotate([0,0,-90])
    	  Base();
    
    }
    
    if (Layout == "Build2") {
    
      translate([0,-LidLength/2,0])
    	Lid(true);
    
    }
    
    if (Layout == "Build3") {
    
      translate([-20,0,0])
    	Shell();
    
    }
    
    if (Layout == "Build4") {
    
      translate([0,0,(PlugBaseThick + PlugFillThick)])
    	rotate([180,0,0])
    	  PlugPlate();
    
    }
    
  • KG-UV3D GPS+Voice: Box

    The previous iteration of GPS+voice interface boxes came from the Sherline CNC mill, with a considerable amount of huffing & puffing. I got the Thing-O-Matic to simplify that process…

    The general idea is to build a box that clips onto the radio in place of the standard battery pack. External power comes into the box and goes directly to the radio’s battery contacts; this will pose a problem with the Wouxun KG-UV3D, because it wants 7.2 V rather than the stepped-up 9 V from the Li-Ion packs I’ve been using. I think a three-wire power cord is in order: +9 V for the interface, +7.2 V for the radio, and common.

    The box also interfaces with the radio’s mic and speaker jacks. Last time around, I made a gluing fixture to keep the plugs in alignment while the epoxy cured around the plugs in the plate, but maybe I can simplify that with 3D printing. Plastic will be better in one respect: the shells of the two plugs must be electrically isolated.

    This first-pass (*) approximation shows the three tabs on the pack that engage the radio’s base:

    KG-UV3D Interface Box prototype - right side
    KG-UV3D Interface Box prototype – right side

    A detail of those tabs, as seen from the bottom:

    KG-UV3D Interface Box prototype - end tabs
    KG-UV3D Interface Box prototype – end tabs

    The ICOM IC-Z1A battery pack had a set of slip-in alignment features that held the pack on the radio, so two strips of tape sufficed to hold the interface box in place. Each Wouxun battery pack includes a spring-loaded latching mechanism that engages a pair of ramped tabs on the radio body that hold the pack against the spring-loaded battery contacts. That means I must come up with an actual latch of some sort to oppose the contact springs, but I haven’t figured that out yet.

    The solid model, with the plug mounting plate floating beside it, looks like this:

    Case Solid Model - Tab End View - Fit
    Case Solid Model – Tab End View – Fit

    Tomorrow, the solid modeling…

    * It’s actually the third printing of the bottom plate with the three tabs and the base plate with the battery contacts. That’s how I figured out the 0. 5% shrinkage thing.

    [Update: The sketch with the dimensions emerged from beneath a pile o’ stuff…]

    Wouxun KG-UV3D Battery Pack Dimensions
    Wouxun KG-UV3D Battery Pack Dimensions

     

  • Thing-O-Matic: Small Features

    Strainer - knob perimeter thread
    Strainer – knob perimeter thread

    It seems most of the stuff I build with my Thing-O-Matic involves small features and thin sections that bump hard against the minimum possible sizes. I’ve found that forcing critical solid model dimensions to be integer multiples of the the extrusion width or thickness stabilizes the whole idea→model→G-Code→object chain by encouraging Skeinforge to make the choices I prefer.

    Or perhaps I’m just constraining my choices to make Skeinforge happiest. One can view reality in many ways…

    Anyhow, my OpenSCAD programs tend to have these lines up near the top:

    ThreadThick = 0.33;
    ThreadWidth = 2.0 * ThreadThick;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    

    The ThreadThick parameter matches the Skeinforge thread thickness parameter(s) and the 2.0 matches the w/t setting(s). Those correspond quite closely to the actual printed results, as tediously verified through many measurements. Throughout the rest of the OpenSCAD program, I compute the dimensions of key features using those sizes as building blocks.

    The IntegerMultiple function returns the next higher multiple of the basic Unit that’s greater-than-or-equal-to the desired Size. Feeding in the thread thickness or width as the Unit ensures that the result will be an integer multiple of the smallest-possible dimension and won’t be smaller. The integer limit happens automagically, because the printer can’t lay down anything else, but a less-than-possible size can cause features to (unpredictably, in my experience) vanish without warning. This way your model reflects the printed reality and Skeinforge seems more likely to produce a predictable result.

    So the parameter controlling the thickness of a flat sheet might look like:

    PlateThick = IntegerMultiple(2.0,ThreadThick);
    

    Given ThreadThick = 0.33, the sheet will be 7 layers thick = 2.31 mm. If the sheet must not exceed 2.0 mm, however, then you need a similar function with floor(), which may eradicate very small features.

    This trick seems most useful for thin wall sections, because the wall width directly affects the fill:

    • Less  than 1 thread width can’t be built
    • Exactly 1 thread width is the thinnest possible wall
    • Widths between 1 and 2 thread widths may be either, depending on surrounding features
    • Exactly 2 thread widths produces a nice wall
    • Widths between 2 and 3 thread widths can’t fill properly
    • Exactly 3 thread widths fills perfectly
    • Over 3 thread widths generally fill properly

    So making the rim around a recessed lid become an integral number of thread widths, with a minimum width of 1.0 mm, looks like this:

    LidMargin = IntegerMultiple(1.0,ThreadWidth);
    

    With a 0.66 mm thread width, the nominal wall is 1.5 threads wide and could print as either 1 or 2 threads, depending on other factors. Rather than leave the results to chance, I force the solid model wall to be exactly 2 threads wide to make the printed result come out at 1.32 mm. Because I don’t care exactly how wide the lid margin is, as long as it’s at least one thread, that’s fine with me.

    Generally, the values come from computations based on other dimensions, so quantizing the results keeps the printed result stable over small variations of those inputs.

    If I ever get around to changing the nozzle to from 0.5 mm to 0.4 mm, I’ll probably change the thread dimensions to 0.25 mm x 0.5 mm (keeping the same 2.0 w/t ratio). A 1.0 mm wall would then still be exactly 2 threads wide and come out looking exactly the same, but with a total width of 1.00 mm.

    That’s the intent, anyway.

  • Stepper Dynamometer: Mechanical Resonance

    Here’s a howling 1200 Hz mechanical resonance excited by a full-step drive at 290 rpm, with the resonance running at 5 cycles for every 4 full steps.:

    1 Step Res 240 Hz
    1 Step Res 240 Hz

    [Update: top trace is voltage from big stepper-as-generator, bottom is small stepper winding current = 1 A/div.]

    Switching to 1/2 microstepping at the same rotational speed pretty well tamps it down:

    Half Step Res 240 Hz
    Half Step Res 240 Hz

    However, here’s the same resonance excited by 1/2 microstepping at half the rotational speed, oscillating at 9 irregular cycles for every 8 half-steps:

    Half res 120 Hz
    Half res 120 Hz

    The resonance was strong enough to back the setscrew completely out of the generator end of the shaft, leaving me wondering why the output was dead when the motor was obviously running. Yes, several times…

  • Stepper Dynamometer: Microstepping Mode vs. Torque

    Conventional wisdom has it that stepper motor torque decreases as the number of microsteps per full step increases. One reasonably careful measurement trumps a kilo-opinion, so here’s a chart (clicky for bigger) of measurements to mull over:

    Microstepping Mode vs Speed - 17PM-J034
    Microstepping Mode vs Speed – 17PM-J034

    Each group of like-colored dots marks the results for full, 1/2, 1/4, 1/8, and 1/16 microstepping with the same load resistance. The caret marks the full-step data point within each group. The load resistances range from a dead short (about 1 Ω due to winding resistance) on the left to 50 Ω on the right.

    While I’ll grant the existence of the occasional data-collection error, it’s pretty obvious that:

    • Torque is reasonably constant regardless of microstepping mode
    • Full-step mode doesn’t produce more torque and, indeed, produces considerably less under heavy loads

    Now, one can argue that the A4988 doesn’t operate in real full-step mode, because it energizes both windings at 1/√2 of the maximum current setting for each full step rather than energizing a single winding at the maximum current. That may be true, but conventional wisdom seems to not bother with such details when opining about torque, either…

    As nearly as I can tell, 1/8 microstepping gives as much torque as you’re likely to get from the motor, while having reasonably smooth motion that avoids exciting mechanical resonances.

    That chart (or one remarkably like it) will appear in an upcoming Circuit Cellar column. The tonnage of data supporting those dots suggests building an automated dynamometer would be a Good Idea …

  • Canon NB-5L Battery Dimensions

    Having acquired a bunch of cheap batteries from the usual eBay suppliers for my new Canon SX230HS pocket camera, it’d be nice to measure their actual (and undoubtedly pathetic) capacity, which implies the need for a holder to make firm contact with the terminals. Sounds like a 3D printer might come in handy for that, doesn’t it?

    The first step: measure the dimensions of actual batteries:

    NB-5L Battery Dimensions
    NB-5L Battery Dimensions

    The terminals lie on what looks to be hard 1/8 inch centers, which must be pure coincidence. They’re recessed anywhere from 0.75 mm to 1.0 mm, depending on who made the thing, into the battery’s endplate.

    The Canon charger has three spring-loaded bent-wire contacts, arranged so the (-) terminal touches first as the battery slides into the holder, then (+), and finally the thermistor (T), with about 0.5 mm between each pair. That spring loading provides enough force to hold the battery in the charger.

    FWIW, the thermistor is 7.5 kΩ w.r.t. (-) at room temperature.

    The plan so far: use three big old gold-plated terminal pins as contacts, with flexible wires to a PowerPole connector that matches the battery tester. Cross-drill the pins to fit music wire lever springs, because the contact spacing is smaller than the smallest coil springs in the Big Box o’ Little Springs. I only need two terminals, so maybe I can force-fit a pair of small coil springs in there, which would be nice.

  • Stepper Dynamometer: Sync Wheel

    This is a modification of that sync gadget to work on the prototype stepper dynamometer. The key differences:

    • The wheel has a notch rather than a flag
    • The opto switch mounts on a sturdy post

    The wheel fits the shaft with a 4-40 setscrew to hold it in place. The post has 4-40 mounting holes for one of those optical switches, plus a big hole for the wiring. The solid models look about like you’d expect:

    Wheel and post solid model
    Wheel and post solid model

    I located the post’s holes on the baseplate with a spindly pair of transfer punches, after transferring the wheel’s centerline by eyeballometric guesstimation:

    Locating holes with transfer punches
    Locating holes with transfer punches

    Then aligned the baseplate on the Sherline, located the holes, and drilled ’em with manual CNC to get the proper spacing:

    Drilling opto switch post holes
    Drilling opto switch post holes

    And then it went together quite smoothly:

    Dyno with sync wheel
    Dyno with sync wheel

    What’s really nice about 3D printing is you can build stuff like this without too much fuss & bother: figure out the solid model, walk gingerly through the software minefield, and (usually) assemble the parts later that day.

    A bit of wiring to power the LED and pull up the phototransistor should do the trick.

    The OpenSCAD code, including a few tweaks that rationalize spacings and sizes and suchlike:

    // Optical Interrupter for stepper dynamometer
    // Suited for low speed demonstrations!
    // Ed Nisley KE4ZNU August 2011
    
    include </home/ed/Thing-O-Matic/lib/MCAD/units.scad>
    include </home/ed/Thing-O-Matic/Useful Sizes.scad>
    
    // Layout options
    
    Layout = "Build";					// Build Show Wheel Switch
    
    //- Extrusion parameters - must match reality!
    //  Print with +2 shells and 3 solid layers
    
    ThreadThick = 0.33;
    ThreadWidth = 2.0 * ThreadThick;
    
    HoleWindage = 0.3;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;
    
    //- Wheel dimensions
    
    ShaftDia = (3/8) * inch;
    ShaftHeight = 27.39;					// from motor mount assembly
    
    HubDia = IntegerMultiple(ShaftDia,ThreadWidth) + 15*ThreadWidth;
    HubLength = IntegerMultiple(10.0,ThreadThick);		// total, not added to plate
    
    SetScrewOffset = 2*Tap4_40;
    SetScrewDia = Tap4_40;
    
    WheelRadius = IntegerMultiple(24.0,ThreadWidth);
    WheelThick = IntegerMultiple(1.5,ThreadThick);
    
    CutoutAngle = (2/50)*360;
    CutoutWidth = WheelRadius*tan(CutoutAngle);
    CutoutDepth = 6.0;
    
    echo(str("Wheel radius: ",WheelRadius," dia: ",2*WheelRadius," thick: ",WheelThick));
    echo(str("Hub sidewall:",(HubDia - ShaftDia)/2));
    echo(str("Cutout width: ",CutoutWidth," angle: ",CutoutAngle," depth: ",CutoutDepth));
    
    //- Optical switch dimensions
    
    OSBaseLength = (31/32) * inch;
    OSBaseWidth = (1/4) * inch;
    OSBaseThick = 0.100 * inch;
    
    OSLeadSpace = 0.300 * inch;							// leads fit though this opening
    
    OSHolesOC = (3/4) * inch;							// switch mounting holes
    OSHoleDia = Tap4_40;
    
    SwHeight = IntegerMultiple((ShaftHeight + OSBaseWidth),ThreadThick);
    SwWidth = IntegerMultiple(OSBaseLength,ThreadWidth);
    SwThick = IntegerMultiple(OSBaseWidth,ThreadWidth);
    
    SwBaseWidth = 4*SwThick;							// pillar base width
    SwBaseOC = IntegerMultiple(2.5*SwThick,1.0);		// base screw spacing
    
    echo(str("Pillar height: ",SwHeight," width: ",SwWidth," thick: ",SwThick));
    echo(str("       base screws: ",SwBaseOC," OC"));
    
    //----------------------
    // 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);
    
    }
    
    module Sector(OutR,InR,Thick,Angle) {
    
      difference() {
    	cylinder(r=OutR,h=Thick);
    	cylinder(r=InR,h=Thick);
    
    	rotate([0,0,CutoutAngle/2])
    	  translate([0,OutR,Thick/2])
    		cube([OutR*2,OutR*2,Thick],center=true);
    	rotate([0,0,-CutoutAngle/2])
    	  translate([0,-OutR,Thick/2])
    		cube([OutR*2,OutR*2,Thick],center=true);
      }
    }
    
    //-- Interrupter Wheel
    
    module Wheel() {
    
      difference() {
    	union() {
    	  cylinder(r=WheelRadius,h=WheelThick);			// base plate
    	  cylinder(r=HubDia/2,h=HubLength);				// hub
    	}
    
    	translate([0,0,-Protrusion])						// shaft hole
    	  PolyCyl(ShaftDia,(HubLength + 2*Protrusion));
    
    	translate([0,0,(HubLength - SetScrewOffset)])
    	  rotate([0,270,0])
    		PolyCyl(SetScrewDia,HubDia);
    
    	translate([0,0,-Protrusion])						// sector cutout
    	  Sector((WheelRadius + Protrusion),
    			(WheelRadius - CutoutDepth),
    			(WheelThick + 2*Protrusion),
    			CutoutAngle);
      }
    
    }
    
    //-- Optical Switch Mount
    
    module SwitchMount() {
    
      difference() {
    	union() {											// mounting pillar
    	  translate([0,0,SwHeight/2])
    		cube([SwThick,SwWidth,SwHeight],center=true);
    	  translate([0,0,SwThick/2])
    		cube([SwBaseWidth,SwWidth,SwThick],center=true);
    	}
    
    	translate([0,0,ShaftHeight]) {
    	  translate([-(SwThick/2 + Protrusion),0,0])		// lead clearance slot
    		rotate([0,90,0])
    		  scale([(OSBaseWidth/OSLeadSpace),1,1])
    			PolyCyl(OSLeadSpace,(SwThick + 2*Protrusion));
    
    	  for (y=[-1,1])									// switch screws
    		translate([0,(y*OSHolesOC/2),0])
    		  rotate([0,90,0])
    			cylinder(r=Tap4_40/2,h=(SwThick + 2*Protrusion),center=true,$fn=6);
    	}
    
    	for (x=[-1,1])										// base screws
    	  translate([(x*SwBaseOC/2),0,-Protrusion])
    		rotate([0,0,(x-1)*90])							// get points outward
    		  PolyCyl(Clear4_40,(SwThick + 2*Protrusion));
      }
    
    }
    
    //-------------------
    // Build it!
    
    ShowPegGrid();
    
    if (Layout == "Build") {
      translate([0,WheelRadius,0])
    	Wheel();
      translate([0,-SwWidth,0])
    	SwitchMount();
    }
    
    if (Layout == "Wheel")
      Wheel();
    
    if (Layout == "Switch")
      SwitchMount();
    
    if (Layout == "Show") {
      translate([0,WheelThick/2,ShaftHeight])
    	rotate([90,0,0])
    	  Wheel();
      translate([(WheelRadius + OSBaseThick + SwThick/2),0,0])
    	SwitchMount();
    }
    

    The sizes differ slightly from the photos, but you’d never see the difference.