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

}

One thought on “KG-UV3D GPS+Voice: Box Model

Comments are closed.