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: Recumbent Bicycling

Cruisin’ the streets

  • Wouxun KG-UV3D GPS+Voice Interface: Improved Case

    This case has a few refinements beyond that one, but it’s recognizably a descendant. The main changes:

    • The HT cable port on the side has a nice polygonal roof to reduce overhang
    • The serial connector sits in a recess to allow a thicker top plate
    • Smaller opening for the LEDs; I’ll get a window in this one, fer shure, yeah
    • 4-40 screws hold the base plate on; setscrews may work and look better

    Looks like I’ll be using blue filament for this version, having just discovered the last of the weird colors in the bottom of the 5 gallon bucket serving as a storage bin.

    A view from the top:

    Solid Model - Oblique Exploded Top
    Solid Model – Oblique Exploded Top

    And from the base:

    Solid Model - Oblique Exploded Base
    Solid Model – Oblique Exploded Base

    The OpenSCAD source code:

    // Wouxun KB-UV3D Battery Pack Case
    // Ed Nisley KE4ZNU July 2012
    
    include </home/ed/Thing-O-Matic/lib/MCAD/units.scad>
    include </home/ed/Thing-O-Matic/Useful Sizes.scad>
    
    // Layout options
    
    Layout = "Show";
    					// Overall layout: Fit Show
    					// Printing plates: Build1 .. Buildn (see bottom!)
    					// Parts: TT3 Audio DSub Shell Base Top
    					// Shapes: RadioBase Contact
    					// Speaker-mic mount: PlugPlate
    
    ShowGap = 10;		// spacing between parts in Show layout
    
    //- Extrusion parameters must match reality!
    //  Print with +1 shells and 3 solid layers
    
    ThreadThick = 0.25;
    ThreadWidth = 2.0 * ThreadThick;
    
    HoleWindage = 0.2;
    
    function IntegerMultiple(Size,Unit) = Unit * ceil(Size / Unit);
    
    Protrusion = 0.1;			// make holes end cleanly
    
    //----------------------
    // Dimensions
    
    CaseOverallHeight = 31.5;				// from battery surface, must clear PCBs!
    CaseOverallWidth = 56;
    CaseOverallLength = 80.25;				// inside of base to end of compartment
    
    BatteryClearance = 1.5;					// contact seal height = air gap to compartment
    
    // Interface to radio battery contacts
    //	Length = shell length
    //		calculated after everything else, so as to fill the compartment
    
    ContactDia = 6.0;				// use rounded contact for simplicity
    ContactRecess = IntegerMultiple(0.75,ThreadThick);	// recess for contact plate
    ContactGapX = 10.5;				// X space between contacts
    Contact1Y = 52.5;				// offset from base to edge of contact
    Contact2Y = 56.5;
    ContactStudDia = Clear4_40;
    ContactStudHead = IntegerMultiple(Head4_40,ThreadWidth);
    ContactStudHeadThick = Head4_40Thick;
    
    PlateWidthMin = 53.0;
    PlateWidthMax = 54.5;
    PlateThick = IntegerMultiple(ContactRecess + ContactStudHeadThick,ThreadThick);
    PlateAngle = atan(PlateThick/(PlateWidthMax/2 - PlateWidthMin/2));
    
    echo("Battery plate thick: ",PlateThick);
    
    // Offsets from battery surface to PCB centerlines
    //	TT3 must be above HT back shell for DB9 clearance
    //	These must cooperate with the numbers in the case shell module
    
    TT3Offset = 17.5 + PlateThick;
    AudioOffset = 4.0 + PlateThick;
    
    // Plate interface to base alignment holes and notches
    
    BaseWidthInner = PlateWidthMin;
    BaseWidthOuter = CaseOverallWidth;
    BaseLength = CaseOverallHeight;					// perpendicular to battery surface
    BaseThick = IntegerMultiple(1.0,ThreadThick);	// minimum sheet thickness below teeth
    BaseWidthTaper = 5.0;							// ramp across entire width
    
    BaseOpeningMax = 43.0;
    BaseOpeningMin = 33.0;
    BaseOpeningY = 5.3;
    BaseOpeningDepth = IntegerMultiple(2.25,ThreadThick);
    
    BaseTotalThick = BaseThick + BaseOpeningDepth;
    echo("Base min thick: ",BaseThick," total: " ,BaseTotalThick);
    
    BaseTabWidth = 6.0;
    BaseTabThick = 2.0;
    BaseTabGap = 7.0;
    BaseTabOC = BaseTabWidth + BaseTabGap;
    BaseToothSection = 3*BaseTabWidth + 2*BaseTabGap;
    
    BaseToothBase = 5.8;
    BaseToothTip = 2.8;
    BaseToothThick = 2.0;
    BaseToothAngle = atan(BaseOpeningDepth/0.6);
    BaseToothOC = BaseTabOC;
    
    WedgeAngle = atan(BaseWidthTaper/((BaseWidthOuter - BaseWidthInner)/2));
    
    BaseEndLip = ThreadThick;			// should be 0.25 mm or so
    BaseEndWidth = (PlateWidthMin - 3*BaseToothBase - 2*BaseToothTip)/2;
    BaseEndAngle = atan((BaseOpeningDepth - BaseEndLip)/BaseOpeningY);
    
    SwitchBody = [8.6,3.7,3.3];			// mode switch
    
    // Plate interface to HT battery latch, cables, and connectors
    
    TopThick = IntegerMultiple(5.5,ThreadThick);	// plate thickness for stiffness behind latch bar
    echo("Top plate thick: ",TopThick);
    
    DB9Recess = TopThick - 4.0;			// recess to max TT3 PCB clearance behind DB9 plate
    
    TabEngageLength = 1.6;				// tab engaging surface length
    TabWidth = 3.0;						//  ... width
    TabEngageHeight = 4.5;				//  ... above battery compartment floor
    TabHeight = 7.5;					// tab ramp top above battery compartment floor
    TabOC = 40.0;
    
    LatchBarWidth = 3.4;				// sliding latch mechanism (brass L stock)
    LatchBarDepth = 3.4;
    LatchBarThick = 0.35;
    
    echo(" ... minimum: ",TopThick - LatchBarDepth);
    
    SplitOffset = TT3Offset - 3.5;
    
    TopBevel = 1.0;						// bevel at top of battery compartment
    TopBevelAngle = 45;
    
    PinOffsetWidth = 2.5;				// choose to center in sides of case shell
    PinOffsetHeight = 13.5;				// above baseplate bottom
    PinDepth = 7.0;						// into case shell
    PinDia = 1.2;
    
    ShellLength = CaseOverallLength - BaseThick - TopThick;
    
    echo("Shell length: ",ShellLength);
    
    // Speaker-mic plug plate
    
    PlugBaseThick = 2.5;				// recess depth
    PlugFillThick = 3.0;				// outer plate thickness
    
    //----------------------
    // 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);
    
    }
    
    //-------------------
    // Component parts
    
    //-----
    // TinyTrak3+ PCB and component envelope
    //	Some dimensions should feed into the case shell, but don't
    
    module TinyTrak3(Length = 1.0) {
    
    PCBThick = 1.6;
    PCBWide = 36.5;
    TopHigh = 9.5;
    TopWide = PCBWide - 1.5;
    BotHigh = 2.5;
    BotWide = 35.0;
    
    PCBx = PCBWide/2;
    PCBy = (PCBThick + HoleWindage)/2;
    URx = TopWide/2;
    URy = PCBy + TopHigh;
    LRx = BotWide/2;
    LRy = PCBy + BotHigh;
    
    linear_extrude(height=Length,center=false,convexity=2) {
    	polygon(points=[[URx,URy],[URx,PCBy],[PCBx,PCBy],[PCBx,-PCBy],[LRx,-PCBy],[LRx,-LRy],
    					[-LRx,-LRy],[-LRx,-PCBy],[-PCBx,-PCBy],[-PCBx,PCBy],[-URx,PCBy],[-URx,URy]
    				   ]);
    }
    }
    
    //-----
    // Interface PCB and component envelope
    //	Some dimensions should feed into the case shell, but don't
    
    module AudioInterface(Length = 1.0) {
    
    PCBThick = 2.0;
    PCBWide = 49.5;
    TopHigh = 9.0 + Protrusion;
    TopWide = 46.0;
    BotHigh = 3.0;
    BotWide = 44.0;
    
    PCBx = PCBWide/2;
    PCBy = (PCBThick + HoleWindage)/2;
    URx = TopWide/2;
    URy = PCBy + TopHigh;
    LRx = BotWide/2;
    LRy = PCBy + BotHigh;
    
    linear_extrude(height=Length,center=false,convexity=2) {
    	polygon(points=[[URx,URy],[URx,PCBy],[PCBx,PCBy],[PCBx,-PCBy],[LRx,-PCBy],[LRx,-LRy],
    					[-LRx,-LRy],[-LRx,-PCBy],[-PCBx,-PCBy],[-PCBx,PCBy],[-URx,PCBy],[-URx,URy]
    				   ]);
    }
    }
    
    //-----
    // DB-9 (DE-9) panel opening
    // http://www.interfacebus.com/Connector_D-Sub_Mechanical_Dimensions.html
    //  DB-9 shell mounts on outside surface of case
    // This is for the solder terminal side
    
    module DSubMin9(Length = 1.0) {
    
    Holex = 0.984/2 * inch;
    HoleDia = Tap4_40;
    
    URx = 0.769/2 * inch;
    URy = 0.432/2 * inch;
    
    	linear_extrude(height=Length,center=false,convexity=3) {
    	  polygon(points=[[URx,URy],[URx,-URy],[-URx,-URy],[-URx,URy]]);
    	  for (x = [-1,1]) {
    		translate([x*Holex,0,0])
    		  rotate(45) circle(r=(HoleDia + HoleWindage)/2,$fn=4);
    	  }
    	}
    
    }
    
    //-----
    // Central case shape
    //	This *should* depend directly on the circuit board sizes, but doesn't
    //	The "Offset" parameters attempt to bottle up all the board sizes
    //	Support in LED window must be hand-fit to work correctly... and isn't needed!
    
    module CaseShell(Length=(ShellLength),Holes="true") {
    
    // Polygon coordinates are in XY plane
    
    URx = 40.0/2;
    URy = CaseOverallHeight;
    
    MRx = CaseOverallWidth/2;
    MRy = 15.0;
    
    LRx = CaseOverallWidth/2;
    LRy = (LRx - PlateWidthMin/2)*tan(PlateAngle);
    
    BRx = PlateWidthMax/2;
    BRy = PlateThick - 0*Protrusion;
    
    PRx = PlateWidthMin/2;				// combined battery plate
    PRy = 0;
    
    ScrewOffset = 20.0;					// from top end of case
    
    LEDWindow = [26.0,5.0,6];			// with case aligned vertically
    LEDOffset = [15,URy,(Length + TopThick - 25.0)];
    
    TrimPot1 = [-14,TT3Offset,(Length + TopThick - 30)];
    TrimPot2 = [-14,TT3Offset,(Length + TopThick - 37.5)];
    
    HTCableDia = 3.5;
    HTCableOffset = AudioOffset + HTCableDia/2 + 1.0;
    
    rotate([90,0,180])
    	union() {
    	  difference() {
    
    		  linear_extrude(height=Length,center=false,convexity=5)
    			polygon(points=[[URx,URy],[MRx,MRy],[LRx,LRy],[BRx,BRy],[PRx,PRy],
    							[-PRx,PRy],[-BRx,BRy],[-LRx,LRy],[-MRx,MRy],[-URx,URy]]);
    
    		if (Holes) {
    		  translate([0,AudioOffset,-Protrusion])
    			AudioInterface(Length + 2*Protrusion);
    
    		  translate([0,TT3Offset,-Protrusion])
    			TinyTrak3(Length + 2*Protrusion);
    
    		  for (y = [TT3Offset,AudioOffset])
    			translate([-CaseOverallWidth,y,(Length - ScrewOffset)])
    			  rotate([0,90,0])
    				rotate(0)					// Z rotation puts point upward for printing
    				PolyCyl(Tap4_40,CaseOverallWidth);
    
    		  translate(LEDOffset)
    			rotate([90,90,0])
    			  translate([-LEDWindow[0]/2,-LEDWindow[1]/2,-Protrusion])
    			  cube(LEDWindow,center=false);
    
    		  for (p = [TrimPot1,TrimPot2])
    			translate(p)
    			  rotate([-90,90,0])				// Y rotation puts point upward for printing
    				PolyCyl(3.0,URy);
    
    		  for (x=[-1,1]) {
    			translate([x*(CaseOverallWidth/2 - PinOffsetWidth),
    					  PinOffsetHeight,
    					  (Length - PinDepth)])
    			  rotate(45)						// align hole sides with case sides
    				  PolyCyl(PinDia,2*TopThick);
    			translate([x*(CaseOverallWidth/2 - PinOffsetWidth),
    					  PinOffsetHeight,
    					  -PlateThick])
    			  rotate(45)						// align hole sides with case sides
    				  PolyCyl(PinDia,(PlateThick + PinDepth));
    		  }
    
    		  for (x=[-1,1])						// setscrews to secure base plate
    			translate([x*(CaseOverallWidth/2 - 3*Tap4_40),
    					  TT3Offset,-Protrusion])
    			  rotate(360/(5*4))
    				PolyCyl(Tap4_40,2*TopThick);
    
    		  translate([-(ContactGapX/2 + ContactDia/2),0,(Contact1Y + ContactDia/2)])
    			rotate([90,0,0])
    			  Contact();
    		  translate([+(ContactGapX/2 + ContactDia/2),0,(Contact2Y + ContactDia/2)])
    			rotate([90,0,0])
    			  Contact();
    
    		  translate([CaseOverallWidth/2,HTCableOffset,(Length - HTCableDia/4)])
    			rotate([0,90,0])
    			  cube([(HTCableDia/2 + Protrusion),HTCableDia,CaseOverallWidth],center=true);
    		  translate([0,HTCableOffset,(Length - HTCableDia/2)])
    			rotate([0,90,0])
    			  cylinder(r=(1/cos(30))*HTCableDia/2,h=CaseOverallWidth,$fn=6);
    		}
    	  }
    
    if (false)
    	  if (Holes)
    		translate(LEDOffset)						// support plug in LED window
    		  rotate([90,90,0])
    			translate([-0.95*LEDWindow[0]/2,-0.80*LEDWindow[1]/2,ThreadWidth/2])
    			  cube([0.95*LEDWindow[0],0.80*LEDWindow[1],2*ThreadWidth],center=false);
    
    	}
    
    }
    
    //-----
    // Battery contact recess
    //  This gets subtracted from the bottom plate in two places
    // 	Align points to print upward
    
    module Contact() {
    
    if (true)
    union() {													// vertical printing with case
    	translate([0,0,-(ContactRecess + Protrusion)/2])
    	  PolyCyl(ContactDia,(ContactRecess + Protrusion),8);
    	translate([0,0,-(PlateThick + Protrusion)])
    	  rotate(60/2)
    	  PolyCyl(ContactStudDia,PlateThick,6);
    	translate([0,0,-(ContactRecess + ContactStudHeadThick/3)])
    	  PolyCyl(ContactStudHead,ContactStudHeadThick,8);				// allow for solder blob
    }
    else
    union() {												// horizontal printing alone
    	translate([0,0,-(ContactRecess - Protrusion)/2])
    	  PolyCyl(ContactDia,(ContactRecess + Protrusion),8);
    	translate([0,0,-(PlateThick + Protrusion)])
    	  PolyCyl(ContactStudDia,(PlateThick + 2*Protrusion));
    	translate([0,0,-(ContactRecess + ContactStudHeadThick/3)])
    	  PolyCyl(ContactStudHead,ContactStudHeadThick,8);				// allow for solder blob
    }
    
    }
    
    //-----
    // 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
    );
    }
    
    //-----
    // Battery pack base
    
    module Base() {
    
    difference() {
    
    	rotate([-90,180,0])						// main case shape
    	  CaseShell(BaseTotalThick,false);
    
    	translate([0,0,BaseThick])				// radio base interface
    	  RadioBase();
    
    	translate([0,0,BaseThick])				// tooth bevel
    	  rotate([(-90 + BaseToothAngle),0,0])
    		translate([0,-0.5,0])
    		  cube([(BaseToothSection + 2*Protrusion),1.0,10],center=true);
    
    	translate([-BaseWidthOuter,				// surface slope
    			  -(BaseThick + BaseEndLip)/tan(BaseEndAngle),0])
    	  rotate([BaseEndAngle,0,0])
    		difference() {
    		  cube([2*BaseWidthOuter,3*BaseOpeningY,BaseOpeningDepth],center=false);
    		  translate([(BaseWidthOuter - (BaseToothSection + 2*Protrusion)/2),0,0])
    			cube([(BaseToothSection + 2*Protrusion),1.2*BaseOpeningY,BaseOpeningDepth],center=false);
    		}
    
    	for (x=[-1,1])							// alignment pin holes
    	  translate([x*(CaseOverallWidth/2 - PinOffsetWidth),PinOffsetHeight,-Protrusion])
    		rotate(45)							// align hole side with plate side
    		  PolyCyl(PinDia,2*TopThick);
    
    	for (x=[-1,1])							// mounting setscews
    	  translate([x*(CaseOverallWidth/2 - 3*Tap4_40),
    				TT3Offset,-Protrusion])
    		rotate(-360/(-5*4))
    		  PolyCyl(Tap4_40,2*TopThick);
    
    	translate([(-SwitchBody[0]/2),TT3Offset,-SwitchBody[2]/2])	// mode switch
    	  scale([1,1,2])
    		cube(SwitchBody);
    
    }
    }
    
    //-----
    // Top plate with latch
    //	Split around TinyTrak3 serial connector
    //	 ... which must be at the same height as in the shell!
    //	The cable hole sizes & locations are entirely ad-hoc
    
    module TopPlate() {
    
    Cable1Dia = 5.0;
    Cable2Dia = 5.0;
    CableHoleLength = TopThick + 2*Protrusion;
    CableHoleZ = -Protrusion;
    
    DB9Plate = [32.0,13.5,1.25];					// plate surrounding connector body
    
    difference() {
    
    	rotate([-90,180,180])
    	  CaseShell(TopThick,false);
    
    	translate([0,-TT3Offset,-Protrusion])
    	  DSubMin9(TopThick + 2*Protrusion);
    
    	translate([0,-TT3Offset,(TopThick - DB9Plate[2]/2)])
    	  cube([DB9Plate[0],DB9Plate[1],(DB9Plate[2] + Protrusion)],center=true);
    
    	translate([-CaseOverallWidth,-SplitOffset,-2*Protrusion])		// split the plate
    	  cube([2*CaseOverallWidth,4*Protrusion,(TopThick + 2*Protrusion)]);
    
    	translate([0,0,(TopThick - TopBevel)])
    	  rotate([-TopBevelAngle,0,0])
    		translate([-CaseOverallWidth,-TopThick,0])
    		  cube([2*CaseOverallWidth,2*TopThick,2*TopThick],center=false);
    
    	for (x=[-1,1])
    	  translate([(x*TabOC/2),
    				(-TabHeight/2 + Protrusion),
    				(TopThick - TabEngageLength/2 + Protrusion/2)])
    		rotate([90,0,0])
    		  cube([TabWidth,
    				(TabEngageLength + Protrusion),
    				(TabHeight + Protrusion)],center=true);
    
    	translate([-CaseOverallWidth,
    			  -(TabEngageHeight + LatchBarWidth - BatteryClearance),
    			  (TopThick - LatchBarDepth)])
    	  cube([2*CaseOverallWidth,(LatchBarWidth + LatchBarThick),(LatchBarDepth + Protrusion)]);
    
    	for (x=[-1,1])
    	  translate([(x*CaseOverallWidth/4),
    				-(TabEngageHeight + LatchBarWidth + Clear2_56/2 - BatteryClearance + Protrusion),
    				0]) {
    		translate([0,0,-Protrusion])
    		  rotate(45)						// align sides with slot
    			PolyCyl(Tap2_56,(TopThick + 2*Protrusion));
    		translate([0,0,(TopThick - LatchBarDepth)])
    		  rotate(60)						// align sides with slot
    			PolyCyl((Head2_56 + Protrusion),TopThick,6);		// extra extra clearance
    	  }
    
    	for (x=[-1,1])
    	  translate([x*(CaseOverallWidth/2 - PinOffsetWidth),-PinOffsetHeight,-Protrusion])
    		rotate(45)						// align hole side with plate side
    		  PolyCyl(PinDia,2*TopThick);
    
    	for (x=[-1,1])						// coincidentally line up with latch tabs
    	  translate([(x*TabOC/2),-(SplitOffset - 3.0),-Protrusion])
    		scale([1,1.7,1])
    		  PolyCyl(Cable1Dia,CableHoleLength,6);
    }
    
    }
    
    //-----
    // Speaker-Mic plug mounting plate
    
    module PlugPlate() {
    
    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;
    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;
    PlugFillRadius1 = 1.5;
    PlugFillRadius2 = 4.5;
    
    PlugFillOffsetYTotal = 0;
    
    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 things...
    
    ShowPegGrid();
    
    if (Layout == "TT3")
    TinyTrak3();
    
    if (Layout == "Audio")
    AudioInterface();
    
    if (Layout == "DSub")
    DSubMin9();
    
    if (Layout == "Shell")
    CaseShell(CaseOverallLength);
    
    if (Layout == "Top")
    TopPlate();
    
    if (Layout == "Base")
    Base();
    
    if (Layout == "RadioBase")
    RadioBase();
    
    if (Layout == "PlugPlate")
    PlugPlate();
    
    if (Layout == "Contact")
    rotate([180,0,0])
    	Contact();
    
    if (Layout == "Show" || Layout == "Fit") {
    
    translate([0,-ShellLength/2,0]) {
    
    	translate([0,(Layout == "Show")?-ShowGap:0,0])
    	  rotate([90,0,0])
    		color("SandyBrown") Base();
    
    	translate([0,0,0])
    	    color("LightGreen") render() CaseShell();
    
    	translate([-(CaseOverallWidth/2 + 10),50,CaseOverallHeight/2])
    	  rotate([0,-90,0])
    		color("Gold") PlugPlate();
    
    	translate([0,((Layout == "Show")?(ShellLength + ShowGap):ShellLength),0])
    	  rotate([-90,0,0])
    		color("BurlyWood") TopPlate();
    }
    }
    
    if (Layout == "Build1") {
    
    translate([5 + CaseOverallHeight,0,0])
    	rotate([0,0,90])
    	  Base();
    
    translate([-(5 + CaseOverallHeight),0,0])
    	rotate(90)
    	  TopPlate();
    
    }
    
    if (Layout == "Build2") {
    
    translate([0,-CaseOverallHeight/2,ShellLength])
    	rotate([-90,0,0])
    		CaseShell();
    
    }
    
    if (Layout == "Build3") {
    
    translate([0,0,(PlugBaseThick + PlugFillThick)])
    	rotate([180,0,0])
    	  PlugPlate();
    
    }
    
  • Wouxun KG-UV3D GPS+Voice Interface: Electronics

    The latest version of the GPS+Voice electronics for the Wouxun KG-UV3D, which I’m getting ready to build:

    Wouxun KG-UV3D GPS+Voice Schematic
    Wouxun KG-UV3D GPS+Voice Schematic

    A few changes:

    • It runs from those 9 V boosted packs, not 7.4 V direct from their lithium cells
    • U2, the MAX4544 data/voice mux, runs from the shunt-regulated +5 V, not the TT3+ regulator
    • Miscellaneous doc cleanup

    I’m mulling over a capacitor between the TT3+ data output and the earbud, so as to monitor transmissions, but I’m not convinced that’s worthwhile.

    The PCB layout, with wire jumpers on the two inner layers:

    Wouxun KG-UV3D GPS+Voice PCB
    Wouxun KG-UV3D GPS+Voice PCB

    The previous version doesn’t look much different from what this one will become:

    GPS-HT Wouxun interface - brassboard
    GPS-HT Wouxun interface – brassboard

    This will replace the ICOM Z-1A radio and GPS interface on Mary’s bike, which has been working fine for quite a while. That can’t last, so I’m trying to get ahead of the failure curve…

    The Wouxun HT GPS+Audio Interface schematic and PCB layout files, both tucked into a ZIP file with an ODT extension. That is not an OpenDocument file: rename it to remove the ODT extension, then unzip it.

  • Repairing Bike Tubes

    Now that I carry a spare tube on the bike to avoid on-the-road patching, a tangle of tubes has been accumulating in the Basement Laboratory. A protracted patching session shows why you can never have too many clamps:

    Repaired bike tubes with clamps
    Repaired bike tubes with clamps

    Yes, I know they’re supposed to be ready to ride immediately after slapping the patch in place. Clamping the patch overnight won’t hurt and might actually help eliminate slow leaks, soooo… after this, they’re rolled up and ready for another season of punctures.

    Repairing tubes goes much easier in the shop than by the side of the road, though. For what it’s worth, those silicone tape pads didn’t help much at all; the tube still eroded at the liner. Grumble…

  • External DVD Battery Capacity: Followup

    A bit over two years ago, those six 9 V 5.4 A·h lithium packs delivered around 4.5 A·h. They’ve been charged and discharged, run down until their undervoltage lockout tripped, severely jounced and bounced, and they still deliver about 4 A·h at 500 mA!

    External Li-Ion packs - 2012-05
    External Li-Ion packs – 2012-05

    That’s a Good Thing, because I haven’t seen anything like those packs since then…

    Never did get around to installing a cutoff switch, as we ride often enough that the penalty for not pulling the plug gets lost in normal use. The Wouxun KG-UV3D seems perfectly happy with 9 V delivered to its battery terminals, providing little motivation to hack into the battery case for a direct tap to the 7.4 V from the cells.

  • HT GPS+Voice Interface: ICOM Z1A vs. W32A vs. Wouxun KG-UV3D

    Here’s a great example of painting yourself into a corner…

    Back in the day, I made a voice-only interface that adapted a helmet-mounted electret mic and earbud to an ICOM IC-Z1A HT. A pair of those let us talk companionably as we rode along.

    Rule One: Never shout at your wife.

    Then I made an interface that combined GPS data (from a Byonics TinyTrak3+ encoder) with the voice audio, all mounted on our Tour Easy recumbents; the interface also supported an external battery for radio power and lived inside a machined case. Eventually, we had two identical radios, interfaces, antennas, and setups on our bikes.

    Along comes our daughter, with her shiny-new Technician amateur radio license. I took an early version of the Z1A interface board, force-fitted it into an early version of the machined case that lacked a top, acquired an ICOM W32A HT and another TT3+, did some tweakage, and defined the result as Good Enough. Time passes, she’s promoted to Larval Engineer, goes off to college, and leaves the bike behind (a faired Tour Easy is ill-suited to being left out in the rain and is not a dorm-room-friendly bike).

    Knowing that the Z1A on my bike is failing, I get a Wouxun KG-UV3D HT and modify the Z1A interface to match. Then I build an interface PCB for the KG-UV3D, conjure up a nice case (which is why I bought a 3D printer), chop the TT3+ out of the W32A lashup, put everything together, and it’s all good.

    Here’s the carcass of the W32A interface in its half-case:

    W32A PCB in case
    W32A PCB in case

    Whereupon our Larval Engineer returns from college and once again needs a radio for her bike. At that point:

    • The W32A interface now lacks its TT3+.
    • The W32A PCB doesn’t fit in the Z1A case
    • The Z1A interface that would fit the W32A radio has the KG-UV3D modifications.
    • The Z1A radio has failed completely; it no longer even turns on.

    Some alternatives:

    1. Get another KG-UV3D, build another interface PCB + case, make it work
    2. Transplant the TT3+ back to the W32A interface
    3. Undo the KG-UV3D mods from the Z1A interface, put it on the W32A

    Given that she’s going to vanish in another three months, tops, Choice 1 is out. Although the transplant in Choice 2 seems straightforward, it requires tedious soldering and produces an interface in a partial case.

    So Choice 3 it is…

    The Z1A board with the KG-UV3D modifications started out like this:

    Z1A PCB modified for Wouxun KG-UV3D
    Z1A PCB modified for Wouxun KG-UV3D

    Un-modified again and back in its machined case:

    Z1A board minus mods - milled case
    Z1A board minus mods – milled case

    Buttoned up and ready to roll:

    Z1A board on W32A - ferrite core
    Z1A board on W32A – ferrite core

    I put a clamp-on ferrite tumor around the GPS receiver cable to keep RF out of the TT3+, which seems quite sensitive to RFI; the poor thing locked up quite dependably on the bench with 5 W into a long rubber duck antenna, but not into a dummy load. The mobile antenna sits relatively far from the radio on the bike, but I think the TT3+ had problems in the early KG-UV3D lashup.

    The TT3 audio level will probably require adjustment, as I’d cranked it up for the KG-UV3D, but that will require some on-the-air testing. Ditto for mic level.

    When I get a KG-UV3D for Mary’s bike, I’ll buy two radios and build two interfaces, so as to finally have a working radio + interface on the shelf.

    I’m mildly tempted by the new Yaesu VX-8GR, but that’s over $350 for a radio that also requires a new interface board design, a new case design, a new set of adapters, and other odds&ends. Not to mention that the radio’s built-in GPS antenna would live at the bottom of the seat frame beside the wheel and below my shoulder. I suppose I could conjure up an entirely new radio mount, but … the deterrents seem overwhelming.

    Various versions of the schematics & PCB layouts for all those boards, plus solid models for the 3D printed case, are scattered here & there on other posts.

  • New Aztek Brake Shoes

    The front brake on my bike started sounding more gritty than usual on a recent ride, which led to pulling the pads off, which led to discovering that one pad had worn completely through:

    Worn-through brake pad
    Worn-through brake pad

    The rim had a slight scuff where that aluminum tab stuck through, but nothing worth worrying about. The wear indicators aren’t reliable, because the pad curve matches 27-ish inch wheels and the Tour Easy has a 20 inch front wheel. If you align the pads to the outside of the rim, as I do, the inside edge gets light wear. So I let ’em wear, check them when the tire gets a flat, and this is the first time a pad has worn through. I think that means the front tire hasn’t had a flat in quite a while…

    While I was at it, I replaced all the pads on both our bikes. The rear pads didn’t have nearly as much wear, which is about what you’d expect, although the wear indicator grooves have just about bottomed out:

    Worn replaceable pads
    Worn replaceable pads

    Those are replaceable pads, which work quite well on the new brake arms. I suspect by the time I get around to needing new inserts (I bought a bunch, of course) they’ll be obsolete and unobtainable.

    I file the pads flat to save a bit of time wearing them in:

    Filed replaceable pads
    Filed replaceable pads

    I don’t hold with the notion of toeing in the pads to avoid squealing, vastly preferring crisp brakes with very little travel. Whatever the material is in Aztek pads, they don’t squeal after they’ve fitted themselves to the rim… but, of course, this new pair howled worse than the Freezer Dog when I got them out on the road.

    Squealing brakes aren’t entirely a bad thing, as they scare the daylights out of oblivious pedestrians, but I’d rather use the bell. So I gripped a strip of fine sandpaper between pad and rim, gently squeezed the brake lever, and rolled the bike about two wheel revolutions. Repeat on the other side and the rim’s now nice and clean and grippy. Flip the sandpaper over, scrub the pad surface, and they don’t make a sound.

    Ding!

  • Isolated Internet Access for Guests

    We provide a camping spot for touring bicyclists riding through the Hudson Valley and, as you’d expect, most of them arrive toting netbooks, tablets, and other net-enabled doodads. While I’m a nice guy and they’re uniformly nice folks, I’d rather not hand them the keys to our house network, so I recently set up a WiFi Internet-only access point that’s firewalled from the LAN.

    The general idea:

    • Use a stock WiFi router to handle DHCP / DNS / WiFi for guests (192.168.2.x)
    • Add a second NIC to the file server as eth1 (192.168.3.1), connected to the router’s WAN port (192.168.3.2)
    • Forward packets between eth0 (house network 192.168.1.x) and eth1, except …
    • Use iptables to prevent router clients from seeing the house network

    The network layout:

    Guest Internet Access Overview
    Guest Internet Access Overview

    The parts came from the Big Box o’ Network Stuff:

    • Linksys / Cisco WRT54G router (Version 8, so OpenWRT won’t run)
    • NetGear 10/100 Mb/s Ethernet PCI card

    The router setup:

    • Static WAN at 192.168.3.2
    • Router base address 192.168.2.1
    • DHCP range 192.168.2.100 through .149, lease time 1 hour
    • DNS entries 4.2.2.1 (L3), 65.88.88.2 (NY Public Library), 129.250.35.250 (NTT)
    • WiFi access to the web admin page disabled (admin only via CAT5 in the Basement Laboratory)
    • Non-broadcast SSID, not that it matters very much
    • WPA2-PSK with an XKCD-style password

    The NIC Just Worked: the drivers come along with the kernel. Because it’s not a general-purpose network interface from the server side, eth1 setup doesn’t require much effort:

    ifconfig eth1 192.168.3.1 netmask 255.255.255.0
    

    I discovered the hard way that trying to define the eth1 interface with Network Manager caused no end of heartache & confusion, not least of which is that having two NICs somehow activates Ubuntu’s internal firewalling & port forwarding. Suffice it to say, just set the NM’s GUI to Ignore the eth1 NIC and do what needs to be done manually.

    With one NIC, Ubuntu runs iptables in “let it be” mode: everything’s allowed, nothing’s blocked, and all packets get forwarded. The tables are empty and the default ACCEPT policy passes everything.

    Adding a rule to the FORWARD chain prevents the router from sending packets to the house network:

    iptables -A FORWARD -i eth1 --destination 192.168.0.0/16 -j REJECT
    

    That still allows a ping response from the file server’s eth0 NIC at 192.168.1.2 back to the WiFi clients, because packets addressed to the server pass through the INPUT chain. This rule squelches those packets:

    iptables -A INPUT -i eth1 --destination 192.168.0.0/16 -j REJECT
    

    Although packet forwarding is enabled by default, another rule turns on the NAT machinery required to shuttle packets between the 192.168.3.x network and the outside world:

    iptables -A POSTROUTING -t nat -j MASQUERADE
    

    While fiddling with iptables rules that involve packet state tracking (which these do, at least implicitly, I think), you must reset the packet state memories to ensure new packets aren’t regarded as part of an established connection. Install the conntrack utilities, then reset the state as needed:

    sudo conntrack -F
    

    And then it Just Worked.

    Now, back in the day, you’d just put those configuration lines in /etc/rc.local and be done with it. Unfortunately, nowadays the upstart process kicks off rc.local well before the system is in a usable state: somewhat before eth0 is active, which means any automagic network-related activity falls flat on its face.

    So an upstart configuration script is in order… more on that later.

    Some useful, albeit occasionally befuddling references:

    One could, of course, buy dedicated hardware to do all that and more, but it’s nothing you couldn’t accomplish with a bit more configuration on a stock Linux box. Heck, you could even serve an Upside-Down-Ternet to anyone who deserves it; the original has some other suggestions that made the big time.

    A tip o’ the cycling helmet to Dragorn of Kismet for getting me started…