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: Thing-O-Matic

Using and tweaking a Makerbot Thing-O-Matic 3D printer

  • KG-UV3D GPS+Voice: Radio Base Interface

    The Wouxun KG-UV3D has three holes along the base that capture three tabs in the battery case, with tapered edges to align the case with the contacts. After a few passes to get the dimensions right, the plate matching those features came out like this:

    Base plate with tabs
    Base plate with tabs

    The solid model shows the edge tapering down to a single layer:

    Case Tab Base - Solid Model
    Case Tab Base – Solid Model

    The compound taper on the corners must match both the base and the sides of the radio. The bottom plate and shell have corresponding tapers that extend across the glued joints:

    Radio interface tapers
    Radio interface tapers

    That worked out surprisingly well, given the small dimensions and odd angles. The tabs, in particular, bumped right up against the 0.66 mm extrusion width; they’re 2.0 mm thick, so there’s barely one thread width inside the perimeter for fill. A bit of filing & slicing removed the usual enlargement at the end / start of each perimeter thread on the tabs, which is entirely acceptable for something this finicky.

    The OpenSCAD source code with dimensions is all part of that post, but here’s the radio base shape that gets subtracted from the plate to make those tabs:

    Radio Base Polygon - solid model
    Radio Base Polygon – solid model

    This seemed easier than adding a bunch of tiny pegs & triangles, but it’s certainly tedious working around a polygon:

    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
    );
    }
    

    Then subtracting that shape and some inclines…

    Radio Base Interface - solid model - thrown together
    Radio Base Interface – solid model – thrown together

    … lets the base plate pop out of this code:

    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);
      }
    }
    

    I’m still doodling the electronics, alas…

  • LILUG Meeting Presentation

    Multicolored Chalk People
    Multicolored Chalk People

    In the admittedly unlikely event you happen to be near the left-center part of Long Island this evening, drop in on my DIY 3D Printing & the Makerbot Thing-O-Matic presentation for the Long Island Linux Users Group meeting and pick up a tchotchke!

    Many thanks to LILUG for ruthlessly eliminating all my objections to leaving the Basement Laboratory…

  • KG-UV3D GPS+Voice: Plug Mounting Plate

    Unlike my old ICOM IC-Z1A, the Wouxun KG-UV3D radio has mic and speaker jacks recessed into the case, so that a custom plug plate can absorb all the stress from forces applied to the cables without wiggling the plugs. Even better, there’s a removable cover with a mounting screw that can hold the new plate in place!

    Wouxun plug mounting plate - overview
    Wouxun plug mounting plate – overview

    The first pass at the mount required a bit of filing, as the deepest part of the recess turns out to be not exactly rectangular. That’s (probably) fixed in the source code:

    Wouxun plug plate - detail
    Wouxun plug plate – detail

    The solid model looks about like you’d expect, with terribly thin side walls between the plugs and the not-quite-rectangular section. The whole affair is asymmetrical around the long axis; the not-quite-rectangular block and hole really are offset:

    Plug Mount Plate - Solid Model
    Plug Mount Plate – Solid Model

    When printed, the thin sections come out one 0.66 mm plastic thread wide:

    Wouxun plug mounting plate - build
    Wouxun plug mounting plate – build

    I spent quite some time iterating through OpenSCAD, RepG, and SkeinLayer to make sure that came out right. This is from a later version with larger recesses around the plugs:

    Plug Mount Plate - skeinlayer
    Plug Mount Plate – skeinlayer

    Some epoxy eased down along the plugs will lock them into the plastic, with an epoxy putty turd over the top to stabilize the cables and terminal connections. That’s a T6 Torx bit to mate with the 2 mm screw (with a captive washer!) pulled from the Small Drawer o’ Salvaged Metric Screws:

    Wouxun plug plate - trial fit
    Wouxun plug plate – trial fit

    The OpenSCAD source code is part of the huge block of code at the bottom of that post, but here’s the relevant section:

    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));
      }
    
    }
    
  • Thing-O-Matic: Delamination

    ABS plastic shrinks as it cools and large objects with thin sections tend to delaminate, as seen in the Barbie Pistol and a few other objects. The box for the GPS+voice interface is four threads thick and 35 mm tall, which provided enough energy to rip the side apart:

    Box wall delamination
    Box wall delamination

    Solvent glue and a clamp shoved it back together again:

    Clamping delamination
    Clamping delamination

    This one was extruded at 190 °C, which works fine for small objects and isn’t quite enough to fuse something like this. I’ll crank it up to 210 °C for the next iteration to see if that improves the result.

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