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:

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.

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.

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.

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();
}
Comments
One response to “KG-UV3D GPS+Voice: Box Model”
[…] OpenSCAD source code is part of the huge block of code at the bottom of that post, but here’s the relevant […]