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: Machine Shop

Mechanical widgetry

  • 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.

  • 3D Printing Presentation for Long Island LUG

    Helmet mirror mount - 3D model - Fit layout
    Helmet mirror mount – 3D model – Fit layout

    The folks at the Long Island LUG asked me to give my DIY 3D Printing & the Makerbot Thing-O-Matic presentation, which will happen 11 October at 8 pm. Details & directions at lilug.org.

    Should you happen to be in the area that evening, drop in and pick up a tchotchke!

  • External USB Case vs OEM DVD Drive Mounting Bracket

    That little Lenovo Q150 doesn’t include an optical drive and, mostly, I don’t need one, but sometimes it’s handy to boot from a CD. I picked up a used DVD burner that also fits my Dell E1405 laptop (should I need a spare) and a tiny USB laptop drive case from the usual eBay sources for a grand total of $17 delivered.

    The drive had a mounting bracket on the back that obviously had to come off, because the bracket screws snuggled right in among the USB adapter electronics:

    E1405 DVD drive bracket vs USB electronics
    E1405 DVD drive bracket vs USB electronics

    In fact, that flat tab with a hole would have clunked up against the back of the case and prevented it from sliding all the way in, but the screws also foiled Plan B: flip the bracket around so the tab goes under the drive where it couldn’t get lost if I needed it again.

    So now the bracket & screws live in a little bag in the Box o’ USB Stuff.

    The DVD drive works fine with just a single USB cable, although the case came with a power-only USB cable, so the latter also lives in the bag with the bracket. Maybe I’ll need it in the unlikely event I actually burn a DVD in that drive?

  • Reversal Zits: Early Action Variations

    While cranking out some Tux Cookie Cutters, I varied the Reversal settings to see what effect they’d have on a single object with a smooth perimeter. I’d previously settled on 25 rpm for 125 ms with no early action, so this series tests three different times with early action turned on.

    Position 1, where the perimeter threads join. Yes, I have Jitter activated and cranked up to something like 10, but it obviously has no effect on this object:

    Position 1 - Reversal 125 100 50 - early
    Position 1 – Reversal 125 100 50 – early

    Position 2, where the nozzle enters from the outside to start a new thread. The snot hanging off the end makes for an ugly wad:

    Position 2 - Reversal 125 100 50 - early
    Position 2 – Reversal 125 100 50 – early

    Position 3, another nozzle entry point:

    Position 3 - Reversal 125 100 50 - early
    Position 3 – Reversal 125 100 50 – early

    Early Reversal action simply doesn’t work well. With retraction times sufficient to prevent drooling, stopping the extruder before the end of the thread produces unacceptable gaps and starting it before reaching the thread produces hanging snots when the nozzle passes over an existing wall.

    Shorter retraction times produce strands all over the object, because the extruder still contains pressurized plastic and drools.

    I’d previously discovered, although I didn’t write up, that unbalanced Reversal times didn’t provide any benefit: inhale and exhale times must be essentially equal to prevent either starving the first part of each thread or serious drooling. So there’s really only one degree of freedom: the total volume of plastic = rpm x duration.

    Perhaps having separate early action times would help: adjust the shutdown and startup delay times independently of the total Reversal inhale/exhale time. Right now, those delays are simply the inhale/exhale times, evidently assuming clean cutoffs and startups, which obviously isn’t the case.

    And, alas, the Reversal Threshold bug remains unfixed, so you (well, I) can’t tell Reversal to not operate across short motions like the end of one thread and the not-quite-adjacent start of the next.

  • Thing-O-Matic: Axis Calibration vs. ABS Shrinkage

    In the process of adapting my HT GPS interface to a Wouxun KG-UV3D radio, I printed some trial-fit pieces that consistently came out a little short. A bit of division showed that the larger pieces tended to be small in the X & Y axes by about 0.5%. This makes no difference for most 3D printed objects, but in this case the pieces must match up precisely with the radio’s existing battery interface layout. Half a percent matters a lot across a 75 mm part.

    The advice found with most calibration pieces seems to boil down to fudging the printer’s steps/mm setting to make the answer come out right. The default Thing-O-Matic calibration (in machines/thingomatic.xml, wherever that’s hidden in your installation) looks like this:

    <axis id="x" length="106" maxfeedrate="6000" homingfeedrate="2500" stepspermm="47.069852" endstops="min"/>  <!-- Pulley dia: 10.82mm / 1/8 step = 1/(10.82 * pi / 1600) -->
    <axis id="y" length="120" maxfeedrate="6000" homingfeedrate="2500" stepspermm="47.069852" endstops="min"/>  <!-- Pulley dia: 10.82mm / 1/8 step = 1/(10.82 * pi / 1600) -->
    

    You will, of course, have twiddled the maxfeedrate, homingfeedrate, and maybe even the comments to make the answers work on your machine.

    Nophead slapped me upside the head when I made the same mistake that produced the stock stepspermm values: the pulley moves the belt by a fixed number of teeth on each revolution, so you just multiply by the belt tooth pitch to find the distance per revolution. Divide that into the number of (micro)steps per revolution and you get the exact stepspermm value. The stock MBI pulleys have 17 teeth and the belt has a 2 mm tooth pitch, so:

    47.05882 step/mm = 1600 step / (17 * 2 mm)

    That differs from the stock value by not very much at all:

    0.999766 = 47.05882 / 47.069852

    Given that these steppers aren’t losing steps (don’t start with me, you know how I get), I’m quite confident that the X and Y stages move by exactly the commanded distance every time.

    The printer uses a heated build plate and the first layer is 0.33 mm, give-or-take about 0.05 mm, and the objects come out with essentially straight vertical walls. However, the walls aren’t quite perfect, tending to be a bit larger where they contact the plate, and I finally asked the obvious question (abs plastic shrinkage), which produces, among many other hits, that useful table.

    The money quote is that ABS shrinks just about exactly 0.5% as it cools. That’s modulo the starting temperature, the molding process, and so forth and so on, but it’s a pretty nice match.

    Therefore, fudging the printer’s scale isn’t appropriate, because that affects everything you might do with it. Such as, for example, the initial homing sequence, which depends on fairly precise locations that must match up with reality and have no shrinkage problems whatsoever.

    Skeinforge’s Scale plugin applies a factor to the object, so that’s (probably) a more appropriate location for this adjustment. The myriad SF settings get broken down by Craft (extrusion, milling, whatever) and material (ABS, PLA, whatever), so if you can keep all that straight, then you can apply the appropriate Scale for each process and material.

    The Scale doc may seem a tad sparse, but the plugin does have separate settings for the XY plane and the Z height. The latter (probably) doesn’t need scaling, because the nozzle height sets the actual extrusion level; the top layer or two will stretch to make the vertical size come out right as the object cools while it’s a-building.

    I’ll toss a 1.005 scale factor into the XY mix and see what horrors that unleashes by way of unintended consequences.

    More on the radio interface & suchlike in a while…